diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000000..2f786ac8eef05 --- /dev/null +++ b/.clang-format @@ -0,0 +1,71 @@ +# the official .clang-format style for https://github.com/taocpp +# +# clang-format-4.0 -i -style=file $(find -name '[^.]*.[hc]pp') + +Language: Cpp +Standard: Cpp11 + +AccessModifierOffset: -3 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum : true + AfterFunction : true + AfterNamespace : true + AfterStruct : true + AfterUnion : true + BeforeCatch : true + BeforeElse : true + IndentBraces : false +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: false +BreakStringLiterals: false +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 0 +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 3 +ContinuationIndentWidth: 3 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +IndentCaseLabels: true +IndentWidth: 3 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +PointerAlignment: Left +ReflowComments: false +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: Never +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: true +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: true +SpacesInParentheses: true +SpacesInSquareBrackets: true +TabWidth: 8 +UseTab: Never diff --git a/.clangd b/.clangd new file mode 100644 index 0000000000000..500c5d0d258d6 --- /dev/null +++ b/.clangd @@ -0,0 +1,89 @@ +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 +# gcc -E -v -xc++ /dev/null +# - -I/nix/store/l2sgvfcyqc1bgnzpz86qw5pjq99j8vlw-libtool-2.5.4/include +# - -I/nix/store/n087ac9g368fbl6h57a2mdd741lshzrc-file-5.46-dev/include +# - -I/nix/store/p7z72c2s722pbw31jmm3y0nwypksb5fj-gnumake-4.4.1/include +# - -I/nix/store/wzwlizg15dwh6x0h3ckjmibdblfkfdzf-flex-2.6.4/include +# - -I/nix/store/8nh579b2yl3sz2yfwyjc9ksb0jb7kwf5-libxslt-1.1.43-dev/include +# - -I/nix/store/cisb0723v3pgp74f2lj07z5d6w3j77sl-libxml2-2.13.8-dev/include +# - -I/nix/store/245c5yscaxyxi49fz9ys1i1apy5s2igz-valgrind-3.24.0-dev/include +# - -I/nix/store/nmxr110602fvajr9ax8d65ac1g40vx1a-curl-8.13.0-dev/include +# - -I/nix/store/slqvy0fgnwmvaq3bxmrvqclph8x909i2-brotli-1.1.0-dev/include +# - -I/nix/store/lchvccw6zl1z1wmhqayixcjcqyhqvyj7-krb5-1.21.3-dev/include +# - -I/nix/store/hybw3vnacqmm68fskbcchrbmj0h4ffv2-nghttp2-1.65.0-dev/include +# - -I/nix/store/2m0s7qxq2kgclyh6cfbflpxm65aga2h4-libidn2-2.3.8-dev/include +# - -I/nix/store/kcgqglb4iax0zh5jlrxmjdik93wlgsrq-openssl-3.4.1-dev/include +# - -I/nix/store/8mlcjg5js2r0zrpdjlfaxax6hyvppgz5-libpsl-0.21.5-dev/include +# - -I/nix/store/1nygjgimkj4wnmydzd6brsw6m0rd7gmx-libssh2-1.11.1-dev/include +# - -I/nix/store/cbdvjyn19y77m8l06n089x30v7irqz3j-zlib-1.3.1-dev/include +# - -I/nix/store/x10zhllc0rhk1s1mhjvsrzvbg55802gj-zstd-1.5.7-dev/include +# - -I/nix/store/8w718rm43x7z73xhw9d6vh8s4snrq67h-python3-3.12.10/include +# - -I/nix/store/1lrgn56jw2yww4bxj0frpgvahqh9i7gl-perf-linux-6.12.35/include +# - -I/nix/store/j87n5xqfj6c03633g7l95lfjq5ynml13-gdb-16.2/include +# - -I/nix/store/ih8dkkw9r7zx5fxg3arh53qc9zs422d1-llvm-21.1.0-dev/include +# - -I/nix/store/rz4bmcm8dwsy7ylx6rhffkwkqn6n8srn-ncurses-6.5-dev/include +# - -I/nix/store/29mcvdnd9s6sp46cjmqm0pfg4xs56rik-zlib-1.3.1-dev/include +# - -I/nix/store/42288hw25sc2gchgc5jp4wfgwisa0nxm-lldb-21.1.0-dev/include +# - -I/nix/store/wpfdp7vzd7h7ahnmp4rvxfcklg4viknl-tcl-8.6.15/include +# - -I/nix/store/4sq2x2770k0xrjshdi6piqrazqjfi5s4-readline-8.2p13-dev/include +# - -I/nix/store/myw381bc9yqd709hpray9lp7l98qmlm1-ncurses-6.5-dev/include +# - -I/nix/store/dvhx24q4icrig4q1v1lp7kzi3izd5jmb-icu4c-76.1-dev/include +# - -I/nix/store/7ld4hdn561a4vkk5hrkdhq8r6rxw8shl-lz4-1.10.0-dev/include +# - -I/nix/store/fnzbi6b8q79faggzj53paqi7igr091w0-util-linux-minimal-2.41-dev/include +# - -I/nix/store/vrdwlbzr74ibnzcli2yl1nxg9jqmr237-linux-pam-1.6.1/include +# - -I/nix/store/qizipyz9y17nr4w4gmxvwd3x4k0bp2rh-libxcrypt-4.4.38/include +# - -I/nix/store/7z8illxfqr4mvwh4l3inik6vdh12jx09-numactl-2.0.18-dev/include +# - -I/nix/store/f6lmz5inbk7qjc79099q4jvgzih7zbhy-openldap-2.6.9-dev/include +# - -I/nix/store/28vmjd90wzd6gij5a1nfj4nqaw191cfg-liburing-2.9-dev/include +# - -I/nix/store/75cyhmjxzx8z7v2z8vrmrydwraf00wyi-libselinux-3.8.1-dev/include +# - -I/nix/store/r25srliigrrv5q3n7y8ms6z10spvjcd9-glibc-2.40-66-dev/include +# - -I/nix/store/ldp1izmflvc74bd4n2svhrd5xrz61wyi-lld-21.1.0-dev/include +# - -I/nix/store/wd5cm50kmlw8n9mq6l1mkvpp8g443a1g-compiler-rt-libc-21.1.0-dev/include +# - -I/nix/store/9ds850ifd4jwcccpp3v14818kk74ldf2-gcc-14.2.1.20250322/include/c++/14.2.1.20250322/ +# - -I/nix/store/9ds850ifd4jwcccpp3v14818kk74ldf2-gcc-14.2.1.20250322/include/c++/14.2.1.20250322//x86_64-unknown-linux-gnu +# - -I/nix/store/9ds850ifd4jwcccpp3v14818kk74ldf2-gcc-14.2.1.20250322/include/c++/14.2.1.20250322//backward +# - -I/nix/store/9ds850ifd4jwcccpp3v14818kk74ldf2-gcc-14.2.1.20250322/lib/gcc/x86_64-unknown-linux-gnu/14.2.1/include +# - -I/nix/store/9ds850ifd4jwcccpp3v14818kk74ldf2-gcc-14.2.1.20250322/include +# - -I/nix/store/9ds850ifd4jwcccpp3v14818kk74ldf2-gcc-14.2.1.20250322/lib/gcc/x86_64-unknown-linux-gnu/14.2.1/include-fixed diff --git a/.envrc b/.envrc new file mode 100644 index 0000000000000..33b05aab1c50c --- /dev/null +++ b/.envrc @@ -0,0 +1,6 @@ +watch_file flake.nix +use flake + +#export MESON_EXTRA_SETUP="-Db_coverage=true" +#export GENINFO_OPTIONS="--ignore-errors inconsistent,gcov" +#export LCOV_OPTIONS="--ignore-errors inconsistent,gcov" diff --git a/.gitignore b/.gitignore index 4e911395fe3ba..8e429d66ca41f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,11 @@ lib*.pc /Release/ /tmp_install/ /portlock/ + +build/ +install/ +test-db/ +.direnv/ +.cache/ +.history + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000000..13566b81b018a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 0000000000000..1f0ef49b4faf4 --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,580 @@ + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000000000..9c69411050eac --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000000..53624c9e1f9ab --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000000000..b0c1c68fbbad6 --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000000..35eb1ddfbbc02 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000..f5d97424c5047 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(gdb) Attach Postgres", + "type": "cppdbg", + "request": "attach", + "program": "${workspaceRoot}/install/bin/postgres", + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000..cc8a64fa9fa85 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "syscache.h": "c" + } +} \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000000..a609589066525 --- /dev/null +++ b/flake.lock @@ -0,0 +1,78 @@ +{ + "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" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1751211869, + "narHash": "sha256-1Cu92i1KSPbhPCKxoiVG5qnoRiKTgR5CcGSRyLpOd7Y=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b43c397f6c213918d6cfe6e3550abfe79b5d1c51", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1757651841, + "narHash": "sha256-Lh9QoMzTjY/O4LqNwcm6s/WSYStDmCH6f3V/izwlkHc=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "ad4e6dd68c30bc8bd1860a27bc6f0c485bd7f3b6", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "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" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000000..aae6d54c4c8cf --- /dev/null +++ b/flake.nix @@ -0,0 +1,45 @@ +{ + 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"; + }; + + outputs = { + self, + nixpkgs, + nixpkgs-unstable, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: let + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + pkgs-unstable = import nixpkgs-unstable { + inherit system; + config.allowUnfree = true; + }; + + shellConfig = import ./shell.nix {inherit pkgs pkgs-unstable system;}; + in { + formatter = pkgs.alejandra; + devShells = { + default = shellConfig.devShell; + gcc = shellConfig.devShell; + clang = shellConfig.clangDevShell; + gcc-musl = shellConfig.muslDevShell; + clang-musl = shellConfig.clangMuslDevShell; + }; + + packages = { + inherit (shellConfig) gdbConfig flameGraphScript pgbenchScript; + }; + + environment.localBinInPath = true; + } + ); +} diff --git a/out.log b/out.log new file mode 100644 index 0000000000000..109808fdca134 --- /dev/null +++ b/out.log @@ -0,0 +1,583 @@ +Null display is "[null]". +Timing is on. +Expanded display is used automatically. +Welcome to PostgreSQL! + +Type :version to see the PostgreSQL version. + +Type :extensions to see the available extensions. + +Type \q to exit. + +SET +Time: 0.485 ms +SET +Time: 0.102 ms +ALTER SYSTEM +Time: 0.304 ms + pg_reload_conf +---------------- + t +(1 row) + +Time: 0.551 ms +psql:test.sql:9: NOTICE: 42710: extension "pageinspect" already exists, skipping +LOCATION: CreateExtension, extension.c:1988 +CREATE EXTENSION +Time: 0.206 ms +DROP FUNCTION +Time: 1.324 ms +DROP FUNCTION +Time: 1.703 ms +CREATE FUNCTION +Time: 2.192 ms +CREATE FUNCTION +Time: 1.003 ms + pg_stat_reset +--------------- + +(1 row) + +Time: 0.405 ms +=== PHASE 1: Setup with Massive Tuples === +CREATE TABLE +Time: 6.849 ms +ALTER TABLE +Time: 0.441 ms +CREATE INDEX +Time: 0.849 ms +INSERT 0 20 +Time: 2.709 ms +psql:test.sql:164: WARNING: 25P01: there is no transaction in progress +LOCATION: EndTransactionBlock, xact.c:4164 +COMMIT +Time: 0.068 ms + phase | context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +----------+--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + BASELINE | SCAN_OPPORTUNISTIC | 506 | 4 | 79 | 15472 | 3906 | 4 | 364 | 53 | 85 | 0 | 0 | 0.79 | 7.719 + BASELINE | UPDATE_FULL_PAGE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + BASELINE | INSERT_SPACE_CHECK | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + BASELINE | SCAN_END | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + BASELINE | MULTI_INSERT | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 +(5 rows) + +Time: 3.167 ms +=== PHASE 2: Create Dead Tuples in Separate Transactions === +BEGIN +Time: 0.055 ms +UPDATE 10 +Time: 1.329 ms +COMMIT +Time: 0.070 ms +--- After First Dead Tuple Creation --- + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 525 | 4 | 79 | 15472 | 3943 | 4 | 376 | 53 | 86 | 6 | 0 | 0.76 | 7.510 + UPDATE_FULL_PAGE | 10 | 0 | 0 | 0 | 15 | 0 | 0 | 0 | 0 | 10 | 0 | 0.00 | 1.500 +(2 rows) + +Time: 0.142 ms + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 531 | 0.75 | INVALID_XACT_XID | 380 | 71.56 + UPDATE_FULL_PAGE | 10 | 0.00 | LOCK_FAILED | 10 | 100.00 +(2 rows) + +Time: 0.633 ms +BEGIN +Time: 0.061 ms +UPDATE 10 +Time: 1.255 ms +COMMIT +Time: 0.074 ms +--- After Second Dead Tuple Creation --- + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 535 | 5 | 89 | 18456 | 4075 | 5 | 380 | 56 | 88 | 6 | 0 | 0.93 | 7.617 + UPDATE_FULL_PAGE | 20 | 0 | 0 | 0 | 29 | 0 | 1 | 9 | 0 | 10 | 0 | 0.00 | 1.450 +(2 rows) + +Time: 0.139 ms + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 535 | 0.93 | INVALID_XACT_XID | 380 | 71.03 + UPDATE_FULL_PAGE | 20 | 0.00 | LOCK_FAILED | 10 | 50.00 +(2 rows) + +Time: 0.141 ms +psql:test.sql:201: INFO: 00000: vacuuming "postgres.public.ultimate_prune_test" +LOCATION: heap_vacuum_rel, vacuumlazy.c:818 +psql:test.sql:201: INFO: 00000: finished vacuuming "postgres.public.ultimate_prune_test": index scans: 1 +pages: 0 removed, 2 remain, 2 scanned (100.00% of total), 0 eagerly scanned +tuples: 10 removed, 20 remain, 0 are dead but not yet removable +removable cutoff: 867, which was 0 XIDs old when operation ended +new relfrozenxid: 865, which is 4 XIDs ahead of previous value +frozen: 0 pages from table (0.00% of total) had 0 tuples frozen +visibility map: 2 pages set all-visible, 0 pages set all-frozen (0 were all-visible) +index scan needed: 1 pages from table (50.00% of total) had 9 dead item identifiers removed +index "ultimate_prune_test_pkey": pages: 2 in total, 0 newly deleted, 0 currently deleted, 0 reusable +index "idx_ultimate_status": pages: 2 in total, 0 newly deleted, 0 currently deleted, 0 reusable +avg read rate: 0.000 MB/s, avg write rate: 122.709 MB/s +buffer usage: 40 hits, 0 reads, 3 dirtied +WAL usage: 11 records, 3 full page images, 25912 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: heap_vacuum_rel, vacuumlazy.c:1145 +psql:test.sql:201: INFO: 00000: vacuuming "postgres.pg_toast.pg_toast_16536" +LOCATION: heap_vacuum_rel, vacuumlazy.c:818 +psql:test.sql:201: INFO: 00000: finished vacuuming "postgres.pg_toast.pg_toast_16536": index scans: 0 +pages: 0 removed, 0 remain, 0 scanned (100.00% of total), 0 eagerly scanned +tuples: 0 removed, 0 remain, 0 are dead but not yet removable +removable cutoff: 867, which was 0 XIDs old when operation ended +new relfrozenxid: 867, which is 6 XIDs ahead of previous value +frozen: 0 pages from table (100.00% of total) had 0 tuples frozen +visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible) +index scan not needed: 0 pages from table (100.00% of total) had 0 dead item identifiers removed +avg read rate: 122.070 MB/s, avg write rate: 0.000 MB/s +buffer usage: 27 hits, 1 reads, 0 dirtied +WAL usage: 1 records, 0 full page images, 258 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: heap_vacuum_rel, vacuumlazy.c:1145 +psql:test.sql:201: INFO: 00000: analyzing "public.ultimate_prune_test" +LOCATION: do_analyze_rel, analyze.c:320 +psql:test.sql:201: INFO: 00000: "ultimate_prune_test": scanned 2 of 2 pages, containing 20 live rows and 0 dead rows; 20 rows in sample, 20 estimated total rows +LOCATION: acquire_sample_rows, analyze.c:1344 +psql:test.sql:201: INFO: 00000: finished analyzing table "postgres.public.ultimate_prune_test" +avg read rate: 0.000 MB/s, avg write rate: 15.625 MB/s +buffer usage: 185 hits, 0 reads, 2 dirtied +WAL usage: 14 records, 2 full page images, 6871 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: do_analyze_rel, analyze.c:841 +VACUUM +Time: 1.017 ms +--- After VACUUM --- + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 636 | 5 | 89 | 18456 | 4147 | 5 | 440 | 56 | 129 | 6 | 0 | 0.79 | 6.520 + UPDATE_FULL_PAGE | 20 | 0 | 0 | 0 | 29 | 0 | 1 | 9 | 0 | 10 | 0 | 0.00 | 1.450 +(2 rows) + +Time: 0.164 ms + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 636 | 0.79 | INVALID_XACT_XID | 440 | 69.18 + UPDATE_FULL_PAGE | 20 | 0.00 | LOCK_FAILED | 10 | 50.00 +(2 rows) + +Time: 0.149 ms +=== PHASE 3: Extreme INSERT Test === +CREATE TABLE +Time: 0.876 ms +ALTER TABLE +Time: 0.171 ms +INSERT 0 1 +Time: 0.253 ms +psql:test.sql:220: WARNING: 25P01: there is no transaction in progress +LOCATION: EndTransactionBlock, xact.c:4164 +COMMIT +Time: 0.064 ms +BEGIN +Time: 0.047 ms +UPDATE 1 +Time: 0.166 ms +COMMIT +Time: 0.056 ms +psql:test.sql:228: INFO: 00000: vacuuming "postgres.public.insert_mega_test" +LOCATION: heap_vacuum_rel, vacuumlazy.c:818 +psql:test.sql:228: INFO: 00000: finished vacuuming "postgres.public.insert_mega_test": index scans: 0 +pages: 0 removed, 1 remain, 1 scanned (100.00% of total), 0 eagerly scanned +tuples: 1 removed, 1 remain, 0 are dead but not yet removable +removable cutoff: 872, which was 0 XIDs old when operation ended +new relfrozenxid: 871, which is 3 XIDs ahead of previous value +frozen: 0 pages from table (0.00% of total) had 0 tuples frozen +visibility map: 1 pages set all-visible, 0 pages set all-frozen (0 were all-visible) +index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed +avg read rate: 0.000 MB/s, avg write rate: 274.123 MB/s +buffer usage: 19 hits, 0 reads, 4 dirtied +WAL usage: 6 records, 4 full page images, 33343 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: heap_vacuum_rel, vacuumlazy.c:1145 +psql:test.sql:228: INFO: 00000: vacuuming "postgres.pg_toast.pg_toast_16552" +LOCATION: heap_vacuum_rel, vacuumlazy.c:818 +psql:test.sql:228: INFO: 00000: finished vacuuming "postgres.pg_toast.pg_toast_16552": index scans: 0 +pages: 0 removed, 0 remain, 0 scanned (100.00% of total), 0 eagerly scanned +tuples: 0 removed, 0 remain, 0 are dead but not yet removable +removable cutoff: 872, which was 0 XIDs old when operation ended +new relfrozenxid: 872, which is 4 XIDs ahead of previous value +frozen: 0 pages from table (100.00% of total) had 0 tuples frozen +visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible) +index scan not needed: 0 pages from table (100.00% of total) had 0 dead item identifiers removed +avg read rate: 137.061 MB/s, avg write rate: 0.000 MB/s +buffer usage: 25 hits, 1 reads, 0 dirtied +WAL usage: 1 records, 0 full page images, 258 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: heap_vacuum_rel, vacuumlazy.c:1145 +psql:test.sql:228: INFO: 00000: analyzing "public.insert_mega_test" +LOCATION: do_analyze_rel, analyze.c:320 +psql:test.sql:228: INFO: 00000: "insert_mega_test": scanned 1 of 1 pages, containing 1 live rows and 0 dead rows; 1 rows in sample, 1 estimated total rows +LOCATION: acquire_sample_rows, analyze.c:1344 +psql:test.sql:228: INFO: 00000: finished analyzing table "postgres.public.insert_mega_test" +avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s +buffer usage: 22 hits, 0 reads, 0 dirtied +WAL usage: 4 records, 0 full page images, 406 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: do_analyze_rel, analyze.c:841 +VACUUM +Time: 0.495 ms +INSERT 0 1 +Time: 0.259 ms +--- After INSERT Test --- + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 734 | 8 | 144 | 22176 | 4217 | 8 | 450 | 72 | 198 | 6 | 0 | 1.09 | 5.745 + UPDATE_FULL_PAGE | 20 | 0 | 0 | 0 | 29 | 0 | 1 | 9 | 0 | 10 | 0 | 0.00 | 1.450 +(2 rows) + +Time: 0.150 ms + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 734 | 1.09 | INVALID_XACT_XID | 450 | 61.31 + UPDATE_FULL_PAGE | 20 | 0.00 | LOCK_FAILED | 10 | 50.00 +(2 rows) + +Time: 0.152 ms +DROP TABLE +Time: 0.684 ms +=== PHASE 4: UPDATE Test with Extreme Sizes === +BEGIN +Time: 0.062 ms +UPDATE 10 +Time: 1.297 ms +COMMIT +Time: 0.068 ms +psql:test.sql:254: INFO: 00000: vacuuming "postgres.public.ultimate_prune_test" +LOCATION: heap_vacuum_rel, vacuumlazy.c:818 +psql:test.sql:254: INFO: 00000: finished vacuuming "postgres.public.ultimate_prune_test": index scans: 0 +pages: 0 removed, 2 remain, 2 scanned (100.00% of total), 0 eagerly scanned +tuples: 10 removed, 20 remain, 0 are dead but not yet removable +removable cutoff: 876, which was 0 XIDs old when operation ended +new relfrozenxid: 866, which is 1 XIDs ahead of previous value +frozen: 0 pages from table (0.00% of total) had 0 tuples frozen +visibility map: 2 pages set all-visible, 0 pages set all-frozen (0 were all-visible) +index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed +avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s +buffer usage: 26 hits, 0 reads, 0 dirtied +WAL usage: 6 records, 0 full page images, 676 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: heap_vacuum_rel, vacuumlazy.c:1145 +psql:test.sql:254: INFO: 00000: vacuuming "postgres.pg_toast.pg_toast_16536" +LOCATION: heap_vacuum_rel, vacuumlazy.c:818 +psql:test.sql:254: INFO: 00000: finished vacuuming "postgres.pg_toast.pg_toast_16536": index scans: 0 +pages: 0 removed, 0 remain, 0 scanned (100.00% of total), 0 eagerly scanned +tuples: 0 removed, 0 remain, 0 are dead but not yet removable +removable cutoff: 876, which was 0 XIDs old when operation ended +new relfrozenxid: 876, which is 9 XIDs ahead of previous value +frozen: 0 pages from table (100.00% of total) had 0 tuples frozen +visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible) +index scan not needed: 0 pages from table (100.00% of total) had 0 dead item identifiers removed +avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s +buffer usage: 12 hits, 0 reads, 0 dirtied +WAL usage: 1 records, 0 full page images, 258 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: heap_vacuum_rel, vacuumlazy.c:1145 +psql:test.sql:254: INFO: 00000: analyzing "public.ultimate_prune_test" +LOCATION: do_analyze_rel, analyze.c:320 +psql:test.sql:254: INFO: 00000: "ultimate_prune_test": scanned 2 of 2 pages, containing 20 live rows and 0 dead rows; 20 rows in sample, 20 estimated total rows +LOCATION: acquire_sample_rows, analyze.c:1344 +psql:test.sql:254: INFO: 00000: finished analyzing table "postgres.public.ultimate_prune_test" +avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s +buffer usage: 40 hits, 0 reads, 0 dirtied +WAL usage: 7 records, 0 full page images, 1020 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: do_analyze_rel, analyze.c:841 +VACUUM +Time: 0.453 ms +BEGIN +Time: 0.060 ms +UPDATE 5 +Time: 0.973 ms +COMMIT +Time: 0.071 ms +--- After UPDATE Test --- + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 824 | 9 | 162 | 24640 | 4251 | 9 | 461 | 77 | 271 | 6 | 0 | 1.09 | 5.159 + UPDATE_FULL_PAGE | 28 | 0 | 0 | 0 | 43 | 0 | 3 | 15 | 0 | 10 | 0 | 0.00 | 1.536 +(2 rows) + +Time: 0.150 ms + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 824 | 1.09 | INVALID_XACT_XID | 461 | 55.95 + UPDATE_FULL_PAGE | 28 | 0.00 | NO_REMOVABLE_XIDS | 15 | 53.57 +(2 rows) + +Time: 0.141 ms +=== PHASE 5: Scan Pressure Test === +BEGIN +Time: 0.052 ms +UPDATE 10 +Time: 1.703 ms +COMMIT +Time: 0.070 ms +psql:test.sql:285: INFO: 00000: vacuuming "postgres.public.ultimate_prune_test" +LOCATION: heap_vacuum_rel, vacuumlazy.c:818 +psql:test.sql:285: INFO: 00000: finished vacuuming "postgres.public.ultimate_prune_test": index scans: 1 +pages: 0 removed, 3 remain, 3 scanned (100.00% of total), 0 eagerly scanned +tuples: 10 removed, 20 remain, 0 are dead but not yet removable +removable cutoff: 879, which was 0 XIDs old when operation ended +new relfrozenxid: 875, which is 9 XIDs ahead of previous value +frozen: 0 pages from table (0.00% of total) had 0 tuples frozen +visibility map: 3 pages set all-visible, 0 pages set all-frozen (0 were all-visible) +index scan needed: 2 pages from table (66.67% of total) had 5 dead item identifiers removed +index "ultimate_prune_test_pkey": pages: 2 in total, 0 newly deleted, 0 currently deleted, 0 reusable +index "idx_ultimate_status": pages: 2 in total, 0 newly deleted, 0 currently deleted, 0 reusable +avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s +buffer usage: 39 hits, 0 reads, 0 dirtied +WAL usage: 10 records, 0 full page images, 870 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: heap_vacuum_rel, vacuumlazy.c:1145 +psql:test.sql:285: INFO: 00000: vacuuming "postgres.pg_toast.pg_toast_16536" +LOCATION: heap_vacuum_rel, vacuumlazy.c:818 +psql:test.sql:285: INFO: 00000: finished vacuuming "postgres.pg_toast.pg_toast_16536": index scans: 0 +pages: 0 removed, 0 remain, 0 scanned (100.00% of total), 0 eagerly scanned +tuples: 0 removed, 0 remain, 0 are dead but not yet removable +removable cutoff: 879, which was 0 XIDs old when operation ended +new relfrozenxid: 879, which is 3 XIDs ahead of previous value +frozen: 0 pages from table (100.00% of total) had 0 tuples frozen +visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible) +index scan not needed: 0 pages from table (100.00% of total) had 0 dead item identifiers removed +avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s +buffer usage: 12 hits, 0 reads, 0 dirtied +WAL usage: 1 records, 0 full page images, 258 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: heap_vacuum_rel, vacuumlazy.c:1145 +psql:test.sql:285: INFO: 00000: analyzing "public.ultimate_prune_test" +LOCATION: do_analyze_rel, analyze.c:320 +psql:test.sql:285: INFO: 00000: "ultimate_prune_test": scanned 3 of 3 pages, containing 20 live rows and 0 dead rows; 20 rows in sample, 20 estimated total rows +LOCATION: acquire_sample_rows, analyze.c:1344 +psql:test.sql:285: INFO: 00000: finished analyzing table "postgres.public.ultimate_prune_test" +avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s +buffer usage: 41 hits, 0 reads, 0 dirtied +WAL usage: 8 records, 0 full page images, 1014 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: do_analyze_rel, analyze.c:841 +VACUUM +Time: 0.590 ms + count +------- + 10 +(1 row) + +Time: 0.286 ms + count +------- + 10 +(1 row) + +Time: 0.126 ms + avg +------------------------ + 10390.0000000000000000 +(1 row) + +Time: 0.647 ms +SET +Time: 0.056 ms +SET +Time: 0.046 ms + count +------- + 20 +(1 row) + +Time: 0.496 ms + count +------- + 5 +(1 row) + +Time: 0.148 ms +RESET +Time: 0.094 ms +RESET +Time: 0.093 ms +--- After SCAN Test --- + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 962 | 10 | 194 | 30552 | 4432 | 10 | 567 | 81 | 298 | 6 | 0 | 1.04 | 4.607 + UPDATE_FULL_PAGE | 39 | 0 | 0 | 0 | 71 | 0 | 5 | 23 | 0 | 11 | 0 | 0.00 | 1.821 +(2 rows) + +Time: 0.164 ms + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 962 | 1.04 | INVALID_XACT_XID | 567 | 58.94 + UPDATE_FULL_PAGE | 39 | 0.00 | NO_REMOVABLE_XIDS | 23 | 58.97 +(2 rows) + +Time: 0.146 ms +=== PHASE 6: Multi-Insert Test === +INSERT 0 20 +Time: 2.848 ms +--- After MULTI_INSERT Test --- + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 962 | 10 | 194 | 30552 | 4432 | 10 | 567 | 81 | 298 | 6 | 0 | 1.04 | 4.607 + UPDATE_FULL_PAGE | 39 | 0 | 0 | 0 | 71 | 0 | 5 | 23 | 0 | 11 | 0 | 0.00 | 1.821 +(2 rows) + +Time: 0.138 ms + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 962 | 1.04 | INVALID_XACT_XID | 567 | 58.94 + UPDATE_FULL_PAGE | 39 | 0.00 | NO_REMOVABLE_XIDS | 23 | 58.97 +(2 rows) + +Time: 0.133 ms +=== PHASE 7: Ultimate Stress Test === +BEGIN +Time: 0.054 ms +UPDATE 20 +Time: 3.228 ms +COMMIT +Time: 0.069 ms +BEGIN +Time: 0.047 ms +UPDATE 20 +Time: 2.787 ms +COMMIT +Time: 0.069 ms +INSERT 0 20 +Time: 3.189 ms +--- After STRESS Test --- + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 999 | 11 | 195 | 31056 | 4490 | 11 | 569 | 113 | 299 | 6 | 1 | 1.10 | 4.494 + UPDATE_FULL_PAGE | 79 | 0 | 0 | 0 | 121 | 0 | 11 | 57 | 0 | 11 | 0 | 0.00 | 1.532 +(2 rows) + +Time: 0.146 ms + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 999 | 1.10 | INVALID_XACT_XID | 569 | 56.96 + UPDATE_FULL_PAGE | 79 | 0.00 | NO_REMOVABLE_XIDS | 57 | 72.15 +(2 rows) + +Time: 0.139 ms +=== PHASE 8: Force Extreme Conditions === +CREATE TABLE +Time: 0.997 ms +ALTER TABLE +Time: 0.173 ms +INSERT 0 1 +Time: 0.216 ms +psql:test.sql:369: WARNING: 25P01: there is no transaction in progress +LOCATION: EndTransactionBlock, xact.c:4164 +COMMIT +Time: 0.067 ms +BEGIN +Time: 0.057 ms +UPDATE 1 +Time: 0.238 ms +COMMIT +Time: 0.060 ms +psql:test.sql:377: INFO: 00000: vacuuming "postgres.public.force_pruning_test" +LOCATION: heap_vacuum_rel, vacuumlazy.c:818 +psql:test.sql:377: INFO: 00000: finished vacuuming "postgres.public.force_pruning_test": index scans: 0 +pages: 0 removed, 1 remain, 1 scanned (100.00% of total), 0 eagerly scanned +tuples: 1 removed, 1 remain, 0 are dead but not yet removable +removable cutoff: 888, which was 0 XIDs old when operation ended +new relfrozenxid: 887, which is 3 XIDs ahead of previous value +frozen: 0 pages from table (0.00% of total) had 0 tuples frozen +visibility map: 1 pages set all-visible, 0 pages set all-frozen (0 were all-visible) +index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed +avg read rate: 0.000 MB/s, avg write rate: 260.417 MB/s +buffer usage: 19 hits, 0 reads, 4 dirtied +WAL usage: 6 records, 4 full page images, 33343 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: heap_vacuum_rel, vacuumlazy.c:1145 +psql:test.sql:377: INFO: 00000: vacuuming "postgres.pg_toast.pg_toast_16561" +LOCATION: heap_vacuum_rel, vacuumlazy.c:818 +psql:test.sql:377: INFO: 00000: finished vacuuming "postgres.pg_toast.pg_toast_16561": index scans: 0 +pages: 0 removed, 0 remain, 0 scanned (100.00% of total), 0 eagerly scanned +tuples: 0 removed, 0 remain, 0 are dead but not yet removable +removable cutoff: 888, which was 0 XIDs old when operation ended +new relfrozenxid: 888, which is 4 XIDs ahead of previous value +frozen: 0 pages from table (100.00% of total) had 0 tuples frozen +visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible) +index scan not needed: 0 pages from table (100.00% of total) had 0 dead item identifiers removed +avg read rate: 124.008 MB/s, avg write rate: 0.000 MB/s +buffer usage: 25 hits, 1 reads, 0 dirtied +WAL usage: 1 records, 0 full page images, 258 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: heap_vacuum_rel, vacuumlazy.c:1145 +psql:test.sql:377: INFO: 00000: analyzing "public.force_pruning_test" +LOCATION: do_analyze_rel, analyze.c:320 +psql:test.sql:377: INFO: 00000: "force_pruning_test": scanned 1 of 1 pages, containing 1 live rows and 0 dead rows; 1 rows in sample, 1 estimated total rows +LOCATION: acquire_sample_rows, analyze.c:1344 +psql:test.sql:377: INFO: 00000: finished analyzing table "postgres.public.force_pruning_test" +avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s +buffer usage: 22 hits, 0 reads, 0 dirtied +WAL usage: 4 records, 0 full page images, 406 bytes, 0 buffers full +system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s +LOCATION: do_analyze_rel, analyze.c:841 +VACUUM +Time: 0.471 ms +INSERT 0 1 +Time: 0.273 ms +--- After FORCE PRUNING Test --- + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 1102 | 11 | 195 | 31056 | 4500 | 11 | 574 | 113 | 397 | 6 | 1 | 1.00 | 4.083 + UPDATE_FULL_PAGE | 79 | 0 | 0 | 0 | 121 | 0 | 11 | 57 | 0 | 11 | 0 | 0.00 | 1.532 +(2 rows) + +Time: 0.146 ms + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 1102 | 1.00 | INVALID_XACT_XID | 574 | 52.09 + UPDATE_FULL_PAGE | 79 | 0.00 | NO_REMOVABLE_XIDS | 57 | 72.15 +(2 rows) + +Time: 0.140 ms +DROP TABLE +Time: 0.573 ms +=== PHASE 9: Final Comprehensive Analysis === + context | calls_total | pages_pruned | tuples_pruned | space_freed | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct +--------------------+-------------+--------------+---------------+-------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------ + SCAN_OPPORTUNISTIC | 1149 | 11 | 195 | 31056 | 11 | 574 | 113 | 444 | 6 | 1 | 0.96 + UPDATE_FULL_PAGE | 79 | 0 | 0 | 0 | 0 | 11 | 57 | 0 | 11 | 0 | 0.00 +(2 rows) + +Time: 0.163 ms + report_type | context | main_failure_reason | failure_count | failure_pct | success_rate_pct +---------------------+--------------------+---------------------+---------------+-------------+------------------ + EXIT_REASON_SUMMARY | SCAN_OPPORTUNISTIC | INVALID_XACT_XID | 574 | 49.96 | 0.96 + EXIT_REASON_SUMMARY | UPDATE_FULL_PAGE | NO_REMOVABLE_XIDS | 57 | 72.15 | 0.00 +(2 rows) + +Time: 0.156 ms + report_type | context | main_failure_reason | recommendation +-----------------+--------------------+---------------------+------------------------------------------------------------ + RECOMMENDATIONS | SCAN_OPPORTUNISTIC | INVALID_XACT_XID | Check prune_xid setting and page header + RECOMMENDATIONS | UPDATE_FULL_PAGE | NO_REMOVABLE_XIDS | Need more transaction churn or longer waits for visibility +(2 rows) + +Time: 0.209 ms + schemaname | relname | n_tup_ins | n_tup_upd | n_tup_hot_upd | n_dead_tup | hot_update_pct | table_size | heap_size +------------+---------------------+-----------+-----------+---------------+------------+----------------+------------+----------- + public | ultimate_prune_test | 0 | 0 | 0 | 0 | 0 | 136 kB | 64 kB +(1 row) + +Time: 2.278 ms +=== Test Complete === +ALTER SYSTEM +Time: 0.132 ms + pg_reload_conf +---------------- + t +(1 row) + +Time: 0.105 ms +DROP TABLE +Time: 0.928 ms diff --git a/pg-aliases.sh b/pg-aliases.sh new file mode 100644 index 0000000000000..221ccdd773846 --- /dev/null +++ b/pg-aliases.sh @@ -0,0 +1,300 @@ +# PostgreSQL Development Aliases + +# Build system management +pg_clean_for_compiler() { + local current_compiler="$(basename $CC)" + local build_dir="$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..." + rm -rf "$build_dir" + mkdir -p "$build_dir" + fi + fi + + mkdir -p "$build_dir" + echo "$current_compiler" >"$build_dir/.compiler_used" +} + +# Core PostgreSQL commands +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 + + 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 "======================================" + # --fatal-meson-warnings + + 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='rm -rf "$PG_BUILD_DIR" "$PG_INSTALL_DIR" && echo "Build and install directories cleaned"' + +# Database management +alias pg-init='rm -rf "$PG_DATA_DIR" && "$PG_INSTALL_DIR/bin/initdb" --debug --no-clean "$PG_DATA_DIR"' +alias pg-start='"$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"' + +# Debugging +alias pg-debug-gdb='gdb -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' + +# Attach to running process +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" -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' + +# Performance profiling and analysis +alias pg-valgrind='valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all "$PG_INSTALL_DIR/bin/postgres" -D "$PG_DATA_DIR"' +alias pg-strace='strace -f -o /tmp/postgres.strace "$PG_INSTALL_DIR/bin/postgres" -D "$PG_DATA_DIR"' + +# Flame graph generation +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' + +# Custom flame graph with specific duration and output +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" +} + +# Benchmarking with pgbench +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' + +# Custom benchmark function +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" +} + +# Benchmark with flame graph +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" + + # Start benchmark in background + pg-bench-run "$clients" 2 1000 "$scale" "$duration" tpcb-like & + local bench_pid=$! + + # Wait a bit for benchmark to start + sleep 5 + + # Generate flame graph for most of the benchmark duration + local flame_duration=$((duration - 10)) + if [ $flame_duration -gt 10 ]; then + pg-flame-generate "$flame_duration" & + local flame_pid=$! + fi + + # Wait for benchmark to complete + wait $bench_pid + + # Wait for flame graph if it was started + if [ -n "${flame_pid:-}" ]; then + wait $flame_pid + fi + + echo "Benchmark and flame graph generation completed" +} + +# Performance 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/,$//")' + +# System performance stats during PostgreSQL operation +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" +} + +# Development 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 + + 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 +} + +alias pg-tidy='find "$PG_SOURCE_DIR" -name "*.c" | head -10 | xargs clang-tidy' + +# Log management +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"' + +# Build logs +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"' + +# Results viewing +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"' + +# Clean up old results +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" +} + +# Information +alias pg-info=' + echo "=== PostgreSQL Development Environment ===" + echo "Source: $PG_SOURCE_DIR" + echo "Build: $PG_BUILD_DIR" + 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: pg-setup, pg-build, pg-install" + echo " Database: pg-init, pg-start, pg-stop, pg-psql" + echo " Debug: pg-debug, pg-attach, pg-valgrind" + echo " Performance: pg-flame, pg-bench, pg-perf" + echo " Benchmarks: pg-bench-quick, pg-bench-standard, pg-bench-heavy" + echo " Flame graphs: pg-flame-30, pg-flame-60, pg-flame-custom" + 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" + echo "=========================================="' + +echo "PostgreSQL aliases loaded. Run 'pg-info' for available commands." diff --git a/post_setup.sql b/post_setup.sql new file mode 100644 index 0000000000000..d8031c3c73417 --- /dev/null +++ b/post_setup.sql @@ -0,0 +1,25 @@ +-- Enable tracking (in case it was reset) +SET enable_heap_prune_tracking = on; + +-- Disable autovacuum on pgbench tables to preserve dead tuples for pruning tests +ALTER TABLE pgbench_accounts SET (autovacuum_enabled = false); +ALTER TABLE pgbench_branches SET (autovacuum_enabled = false); +ALTER TABLE pgbench_tellers SET (autovacuum_enabled = false); +ALTER TABLE pgbench_history SET (autovacuum_enabled = false); + +-- Add columns that can be updated without affecting indexes (for HOT updates) +ALTER TABLE pgbench_accounts ADD COLUMN IF NOT EXISTS last_updated timestamp DEFAULT now(); +ALTER TABLE pgbench_accounts ADD COLUMN IF NOT EXISTS update_count int DEFAULT 0; +ALTER TABLE pgbench_accounts ADD COLUMN IF NOT EXISTS notes text DEFAULT 'initial'; + +-- Make the notes column much larger to create page pressure +ALTER TABLE pgbench_accounts ALTER COLUMN notes SET DEFAULT repeat('initial_data', 100); + +-- Create a partial index to allow more HOT updates +CREATE INDEX IF NOT EXISTS idx_accounts_high_balance ON pgbench_accounts(abalance) WHERE abalance > 1000; + +-- Reset statistics after setup +SELECT pg_stat_reset(); + +-- Show initial state +SELECT 'POST_SETUP_BASELINE' as phase, * FROM capture_prune_stats(); diff --git a/prune_test.sql b/prune_test.sql new file mode 100644 index 0000000000000..b06e7995a116e --- /dev/null +++ b/prune_test.sql @@ -0,0 +1,34 @@ +\set aid random(1, 100000 * :scale) +\set note_suffix random(1, 1000) +\set large_data_size random(50, 200) + +-- Single transaction with multiple updates to same row to create dead tuples and page pressure +BEGIN; + +-- Update the same row multiple times with increasingly large data to create dead tuples +UPDATE pgbench_accounts +SET last_updated = now(), + update_count = update_count + 1, + notes = 'v1_' || :note_suffix || '_' || repeat('data_chunk_', :large_data_size) +WHERE aid = :aid; + +UPDATE pgbench_accounts +SET last_updated = now(), + update_count = update_count + 2, + notes = 'v2_' || :note_suffix || '_' || repeat('more_data_chunk_', :large_data_size + 10) +WHERE aid = :aid; + +UPDATE pgbench_accounts +SET last_updated = now(), + update_count = update_count + 3, + notes = 'v3_' || :note_suffix || '_' || repeat('final_data_chunk_', :large_data_size + 20) +WHERE aid = :aid; + +-- Also update a few nearby rows to create more page pressure +UPDATE pgbench_accounts +SET last_updated = now(), + update_count = update_count + 1, + notes = 'neighbor_' || :note_suffix || '_' || repeat('neighbor_data_', :large_data_size) +WHERE aid BETWEEN :aid + 1 AND :aid + 3; + +COMMIT; diff --git a/results.log b/results.log new file mode 100644 index 0000000000000..de72172764318 --- /dev/null +++ b/results.log @@ -0,0 +1,67 @@ + phase | context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +----------+--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + BASELINE | SCAN_OPPORTUNISTIC | 153 | 0 | 0 | 0 | 0 | 0 | 150 | 0 | 3 | 0 | 0 | 0.00 | 0.000 + BASELINE | UPDATE_FULL_PAGE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + BASELINE | INSERT_SPACE_CHECK | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + BASELINE | SCAN_END | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + BASELINE | MULTI_INSERT | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 +(5 rows) + + phase | context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +------------------------+--------------------+-------------+------------------+---------------------+---------------+------------- + BASELINE_EXIT_ANALYSIS | SCAN_OPPORTUNISTIC | 160 | 0.00 | INVALID_XACT_XID | 157 | 98.13 +(1 row) + + phase | context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +----------+--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + MID_TEST | SCAN_OPPORTUNISTIC | 154 | 0 | 0 | 0 | 1 | 0 | 151 | 0 | 3 | 0 | 0 | 0.00 | 0.006 + MID_TEST | UPDATE_FULL_PAGE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + MID_TEST | INSERT_SPACE_CHECK | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + MID_TEST | SCAN_END | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + MID_TEST | MULTI_INSERT | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 +(5 rows) + + phase | context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +------------------------+--------------------+-------------+------------------+---------------------+---------------+------------- + MID_TEST_EXIT_ANALYSIS | SCAN_OPPORTUNISTIC | 160 | 0.00 | INVALID_XACT_XID | 157 | 98.13 +(1 row) + + phase | context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +-----------+--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + POST_TEST | SCAN_OPPORTUNISTIC | 153 | 0 | 0 | 0 | 1 | 0 | 150 | 0 | 3 | 0 | 0 | 0.00 | 0.007 + POST_TEST | UPDATE_FULL_PAGE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + POST_TEST | INSERT_SPACE_CHECK | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + POST_TEST | SCAN_END | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + POST_TEST | MULTI_INSERT | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 +(5 rows) + + phase | context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +-------------------------+--------------------+-------------+------------------+---------------------+---------------+------------- + POST_TEST_EXIT_ANALYSIS | SCAN_OPPORTUNISTIC | 160 | 0.00 | INVALID_XACT_XID | 157 | 98.13 +(1 row) + + report_type | context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +---------------------+--------------------+-------------+------------------+---------------------+---------------+------------- + EXIT_REASON_SUMMARY | SCAN_OPPORTUNISTIC | 160 | 0.00 | INVALID_XACT_XID | 157 | 98.13 +(1 row) + + report_type | context | main_failure_reason | recommendation | failure_pct +-----------------+--------------------+---------------------+---------------------------------------------------------------------------+------------- + RECOMMENDATIONS | SCAN_OPPORTUNISTIC | INVALID_XACT_XID | Check prune_xid setting and page header - may need transaction visibility | 98.58 +(1 row) + + schemaname | relname | n_tup_ins | n_tup_upd | n_tup_hot_upd | hot_update_pct | table_size +------------+------------------+-----------+-----------+---------------+----------------+------------ + public | pgbench_accounts | 0 | 7786123 | 6686906 | 85.88 | 1932 MB +(1 row) + + relpages | reltuples | n_dead_tup | last_vacuum | last_autovacuum | heap_size +----------+-----------+------------+-------------+-----------------+----------- + 81968 | 5e+06 | 1355592 | | | 1820 MB +(1 row) + + context | calls_total | pages_pruned | tuples_pruned | space_freed | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 153 | 0 | 0 | 0 | 0 | 150 | 0 | 3 | 0 | 0 | 0.00 | 0.144 +(1 row) + diff --git a/run_test.sh b/run_test.sh new file mode 100755 index 0000000000000..2f64f3e036f65 --- /dev/null +++ b/run_test.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash + +set -euo pipefail +#set -x + +PSQL="$PG_INSTALL_DIR/bin/psql -X -h $PG_DATA_DIR postgres" +PGBENCH="$PG_INSTALL_DIR/bin/pgbench -h $PG_DATA_DIR" + +PGBENCH_SCALE=50 +PGBENCH_CLIENTS=8 +PGBENCH_JOBS=4 +PGBENCH_TIME=120 + +echo "=== Setting up test environment ===" +$PSQL -f setup.sql + +echo "=== Setup the benchmark ===" +$PGBENCH -i -s $PGBENCH_SCALE postgres + +echo "=== Post-setup configuration ===" +$PSQL -f post_setup.sql + +echo "=== Pre-loading data to create page pressure ===" +$PSQL -c " +-- Create more update activity to fill pages with large tuples +UPDATE pgbench_accounts +SET notes = 'preload_' || (random() * 10000)::int || '_' || repeat('large_data_chunk', 50), + update_count = update_count + 1, + last_updated = now() +WHERE aid IN ( + SELECT (random() * (100000 * $PGBENCH_SCALE))::int + 1 + FROM generate_series(1, 20000) +); + +-- Create some dead tuples by updating again +UPDATE pgbench_accounts +SET notes = 'dead_preload_' || (random() * 10000)::int || '_' || repeat('dead_data_chunk', 60), + update_count = update_count + 2 +WHERE aid IN ( + SELECT (random() * (100000 * $PGBENCH_SCALE))::int + 1 + FROM generate_series(1, 10000) +); +" + +echo "=== Capturing baseline statistics ===" +$PSQL -c "SELECT 'BASELINE' as phase, * FROM capture_prune_stats();" >results.log +$PSQL -c "SELECT 'BASELINE_EXIT_ANALYSIS' as phase, * FROM analyze_exit_reasons();" >>results.log + +echo "=== Running pgbench HOT update workload ===" +$PGBENCH -c $PGBENCH_CLIENTS -j $PGBENCH_JOBS -T $PGBENCH_TIME -f prune_test.sql -P 10 postgres + +echo "=== Capturing mid-test statistics ===" +$PSQL -c "SELECT 'MID_TEST' as phase, * FROM capture_prune_stats();" >>results.log +$PSQL -c "SELECT 'MID_TEST_EXIT_ANALYSIS' as phase, * FROM analyze_exit_reasons();" >>results.log + +echo "=== Running standard TPC-B workload for comparison ===" +$PGBENCH -c $PGBENCH_CLIENTS -j $PGBENCH_JOBS -T 30 -P 10 postgres + +echo "=== Capturing post-test statistics ===" +$PSQL -c "SELECT 'POST_TEST' as phase, * FROM capture_prune_stats();" >>results.log +$PSQL -c "SELECT 'POST_TEST_EXIT_ANALYSIS' as phase, * FROM analyze_exit_reasons();" >>results.log + +echo "=== Exit Reason Analysis and Recommendations ===" +$PSQL -c " +SELECT + 'EXIT_REASON_SUMMARY' as report_type, + context, + calls_total, + success_rate_pct, + main_failure_reason, + failure_count, + failure_pct +FROM analyze_exit_reasons() +ORDER BY calls_total DESC; +" >>results.log + +$PSQL -c " +SELECT + 'RECOMMENDATIONS' as report_type, + context, + main_failure_reason, + recommendation, + failure_pct +FROM get_pruning_recommendations() +ORDER BY failure_pct DESC; +" >>results.log + +echo "=== HOT Update Effectiveness ===" +$PSQL -c " +SELECT + schemaname, relname, + n_tup_ins, n_tup_upd, n_tup_hot_upd, + CASE WHEN n_tup_upd > 0 + THEN round(100.0 * n_tup_hot_upd / n_tup_upd, 2) + ELSE 0 + END as hot_update_pct, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as table_size +FROM pg_stat_user_tables +WHERE relname = 'pgbench_accounts'; +" >>results.log + +echo "=== Page-level analysis ===" +$PSQL -c " +SELECT + relpages, + reltuples, + n_dead_tup, + last_vacuum, + last_autovacuum, + pg_size_pretty(pg_relation_size('pgbench_accounts')) as heap_size +FROM pg_stat_user_tables +JOIN pg_class ON pg_class.relname = pg_stat_user_tables.relname +WHERE pg_stat_user_tables.relname = 'pgbench_accounts'; +" >>results.log + +echo "=== Detailed Statistics Breakdown ===" +$PSQL -c " +SELECT + context, + calls_total, + pages_pruned, + tuples_pruned, + space_freed, + exit_success, + exit_invalid_xact_xid, + exit_no_removable_xids, + exit_page_not_prunable, + exit_lock_failed, + exit_other, + prune_success_rate_pct, + avg_time_per_call_us +FROM capture_prune_stats() +WHERE calls_total > 0 +ORDER BY calls_total DESC; +" >>results.log + +echo "=== Test complete. Results in results.log ===" +cat results.log diff --git a/setup.sql b/setup.sql new file mode 100644 index 0000000000000..b79f9f1981511 --- /dev/null +++ b/setup.sql @@ -0,0 +1,158 @@ +-- Enable tracking +SET enable_heap_prune_tracking = on; +SET log_min_messages = debug2; + +-- Drop existing functions to avoid signature conflicts +DROP FUNCTION IF EXISTS capture_prune_stats(); +DROP FUNCTION IF EXISTS analyze_exit_reasons(); + +-- Function to capture current statistics with exit reasons +CREATE OR REPLACE FUNCTION capture_prune_stats() +RETURNS TABLE( + context text, + calls_total bigint, + pages_pruned bigint, + tuples_pruned bigint, + space_freed bigint, + time_spent_us bigint, + exit_success bigint, + exit_invalid_xact_xid bigint, + exit_no_removable_xids bigint, + exit_page_not_prunable bigint, + exit_lock_failed bigint, + exit_other bigint, + prune_success_rate_pct numeric, + avg_time_per_call_us numeric +) AS $$ +BEGIN + RETURN QUERY + SELECT + s.context, + s.calls_total, + s.pages_pruned, + s.tuples_pruned, + s.space_freed, + s.time_spent_us, + s.exit_success, + s.exit_invalid_xact_xid, + s.exit_no_removable_xids, + s.exit_page_not_prunable, + s.exit_lock_failed, + s.exit_other, + CASE WHEN s.calls_total > 0 + THEN round(100.0 * s.pages_pruned / s.calls_total, 2) + ELSE 0 + END as prune_success_rate_pct, + CASE WHEN s.calls_total > 0 + THEN round(s.time_spent_us::numeric / s.calls_total, 3) + ELSE 0 + END as avg_time_per_call_us + FROM pg_stat_get_heap_prune_stats() AS s( + context text, + calls_total bigint, + pages_pruned bigint, + tuples_pruned bigint, + space_freed bigint, + time_spent_us bigint, + exit_success bigint, + exit_invalid_xact_xid bigint, + exit_no_removable_xids bigint, + exit_page_not_prunable bigint, + exit_lock_failed bigint, + exit_other bigint + ) + ORDER BY s.calls_total DESC; +END; +$$ LANGUAGE plpgsql; + +-- Function to analyze exit reasons +CREATE OR REPLACE FUNCTION analyze_exit_reasons() +RETURNS TABLE( + context text, + calls_total bigint, + success_rate_pct numeric, + main_failure_reason text, + failure_count bigint, + failure_pct numeric +) AS $$ +BEGIN + RETURN QUERY + WITH exit_analysis AS ( + SELECT + s.context as ctx, + s.calls_total as total_calls, + s.exit_success, + s.exit_invalid_xact_xid, + s.exit_no_removable_xids, + s.exit_page_not_prunable, + s.exit_lock_failed, + s.exit_other, + CASE WHEN s.calls_total > 0 + THEN round(100.0 * s.exit_success / s.calls_total, 2) + ELSE 0 + END as success_rate + FROM capture_prune_stats() s + WHERE s.calls_total > 0 + ), + failure_reasons AS ( + SELECT + ea.ctx, + ea.total_calls, + ea.success_rate, + CASE + WHEN ea.exit_invalid_xact_xid >= GREATEST(ea.exit_no_removable_xids, ea.exit_page_not_prunable, ea.exit_lock_failed, ea.exit_other) + THEN 'INVALID_XACT_XID' + WHEN ea.exit_no_removable_xids >= GREATEST(ea.exit_invalid_xact_xid, ea.exit_page_not_prunable, ea.exit_lock_failed, ea.exit_other) + THEN 'NO_REMOVABLE_XIDS' + WHEN ea.exit_page_not_prunable >= GREATEST(ea.exit_invalid_xact_xid, ea.exit_no_removable_xids, ea.exit_lock_failed, ea.exit_other) + THEN 'PAGE_NOT_PRUNABLE' + WHEN ea.exit_lock_failed >= GREATEST(ea.exit_invalid_xact_xid, ea.exit_no_removable_xids, ea.exit_page_not_prunable, ea.exit_other) + THEN 'LOCK_FAILED' + ELSE 'OTHER' + END as main_reason, + GREATEST(ea.exit_invalid_xact_xid, ea.exit_no_removable_xids, ea.exit_page_not_prunable, ea.exit_lock_failed, ea.exit_other) as max_failure_count + FROM exit_analysis ea + ) + SELECT + fr.ctx, + fr.total_calls, + fr.success_rate, + fr.main_reason, + fr.max_failure_count, + CASE WHEN fr.total_calls > 0 + THEN round(100.0 * fr.max_failure_count / fr.total_calls, 2) + ELSE 0 + END as failure_percentage + FROM failure_reasons fr + ORDER BY fr.total_calls DESC; +END; +$$ LANGUAGE plpgsql; + +-- Function to provide recommendations based on exit reasons +CREATE OR REPLACE FUNCTION get_pruning_recommendations() +RETURNS TABLE( + context text, + main_failure_reason text, + recommendation text, + failure_pct numeric +) AS $$ +BEGIN + RETURN QUERY + SELECT + ar.context, + ar.main_failure_reason, + CASE ar.main_failure_reason + WHEN 'INVALID_XACT_XID' THEN 'Check prune_xid setting and page header - may need transaction visibility' + WHEN 'NO_REMOVABLE_XIDS' THEN 'Need more transaction churn or longer waits for visibility - try VACUUM or wait' + WHEN 'PAGE_NOT_PRUNABLE' THEN 'Check PageHasPrunable() logic - page may not have dead tuples' + WHEN 'LOCK_FAILED' THEN 'Increase lock acquisition attempts or reduce contention' + ELSE 'Investigate other failure causes in logs' + END as recommendation, + ar.failure_pct + FROM analyze_exit_reasons() ar + WHERE ar.calls_total > 0; +END; +$$ LANGUAGE plpgsql; + +-- Reset statistics +SELECT pg_stat_reset(); diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000000000..e503425eb9475 --- /dev/null +++ b/shell.nix @@ -0,0 +1,814 @@ +{ + pkgs, + pkgs-unstable, + system, +}: let + # Use LLVM for modern PostgreSQL development + llvmPkgs = pkgs-unstable.llvmPackages_21; + + # Configuration constants + config = { + pgSourceDir = "$PWD"; + pgBuildDir = "$PWD/build"; + pgInstallDir = "$PWD/install"; + pgDataDir = "/tmp/test-db-$(basename $PWD)"; + pgBenchDir = "/tmp/pgbench-results-$(basename $PWD)"; + pgFlameDir = "/tmp/flame-graphs-$(basename $PWD)"; + }; + + # Single dependency function that can be used for all environments + getPostgreSQLDeps = muslLibs: + with pkgs; + [ + # Build system (always use host tools) + pkgs-unstable.meson + pkgs-unstable.ninja + pkg-config + autoconf + libtool + git + which + binutils + gnumake + + # Parser/lexer tools + bison + flex + + # 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 + htop + iotop + sysstat + ccache + cppcheck + compdb + + # GCC/GDB +# pkgs-unstable.gcc15 + gcc + gdb + + # LLVM toolchain + llvmPkgs.llvm + llvmPkgs.llvm.dev + llvmPkgs.clang-tools + llvmPkgs.lldb + + # Language support + (python3.withPackages (ps: with ps; [requests browser-cookie3])) + tcl + ] + ++ ( + if muslLibs + then [ + # Musl target libraries for cross-compilation + pkgs.pkgsMusl.readline + pkgs.pkgsMusl.zlib + pkgs.pkgsMusl.openssl + pkgs.pkgsMusl.icu + pkgs.pkgsMusl.lz4 + pkgs.pkgsMusl.zstd + pkgs.pkgsMusl.libuuid + pkgs.pkgsMusl.libkrb5 + pkgs.pkgsMusl.linux-pam + pkgs.pkgsMusl.libxcrypt + ] + else [ + # Glibc target libraries + readline + zlib + openssl + icu + lz4 + zstd + libuuid + libkrb5 + linux-pam + libxcrypt + numactl + openldap + liburing + libselinux + glibc + glibc.dev + ] + ); + + # 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 + + 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 + ''; + + # Development shell (GCC + glibc) + devShell = pkgs.mkShell { + name = "postgresql-dev"; + buildInputs = + (getPostgreSQLDeps false) + ++ [ + flameGraphScript + pgbenchScript + ]; + + shellHook = let + icon = "f121"; + in '' + # 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 + export CCACHE_DIR=$HOME/.ccache/pg/$(basename $PWD) + mkdir -p "$CCACHE_DIR" + + # 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}" + + # Development tools in PATH + export PATH=${pkgs.clang-tools}/bin:$PATH + export PATH=${pkgs.cppcheck}/bin:$PATH + + # PosgreSQL Development CFLAGS + # -DRELCACHE_FORCE_RELEASE -DCATCACHE_FORCE_RELEASE -fno-omit-frame-pointer -fno-stack-protector -DUSE_VALGRIND + export CFLAGS="" + export CXXFLAGS="" + + # Python UV + UV_PYTHON_DOWNLOADS=never + + # GCC configuration (default compiler) + export CC="${pkgs.gcc}/bin/gcc" + export CXX="${pkgs.gcc}/bin/g++" + + # PostgreSQL environment + export PG_SOURCE_DIR="${config.pgSourceDir}" + export PG_BUILD_DIR="${config.pgBuildDir}" + 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}" + + # Performance tools in PATH + export PATH="${flameGraphScript}/bin:${pgbenchScript}/bin:$PATH" + + # Create output directories + mkdir -p "$PG_BENCH_DIR" "$PG_FLAME_DIR" + + # Compiler verification + echo "Environment configured:" + echo " Compiler: $CC" + echo " libc: glibc" + echo " LLVM: $(llvm-config --version 2>/dev/null || echo 'not available')" + + # 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 (GCC + glibc)" + echo "Run 'pg-info' for available commands" + ''; + }; + + # Clang + glibc variant + clangDevShell = pkgs.mkShell { + name = "postgresql-clang-glibc"; + buildInputs = + (getPostgreSQLDeps false) + ++ [ + llvmPkgs.clang + llvmPkgs.lld + llvmPkgs.compiler-rt + flameGraphScript + pgbenchScript + ]; + + shellHook = let + icon = "f121"; + in '' + # 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 + export CCACHE_DIR=$HOME/.ccache_pg_dev_clang + mkdir -p "$CCACHE_DIR" + + # 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}" + + # Development tools in PATH + export PATH=${pkgs.clang-tools}/bin:$PATH + export PATH=${pkgs.cppcheck}/bin:$PATH + + # Clang + glibc configuration - use system linker instead of LLD for compatibility + export CC="${llvmPkgs.clang}/bin/clang" + export CXX="${llvmPkgs.clang}/bin/clang++" + + # Use system linker and standard runtime + #export CFLAGS="" + #export CXXFLAGS="" + #export LDFLAGS="" + + # PostgreSQL environment + export PG_SOURCE_DIR="${config.pgSourceDir}" + export PG_BUILD_DIR="${config.pgBuildDir}" + 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}" + + # Performance tools in PATH + export PATH="${flameGraphScript}/bin:${pgbenchScript}/bin:$PATH" + + # Create output directories + mkdir -p "$PG_BENCH_DIR" "$PG_FLAME_DIR" + + # Compiler verification + echo "Environment configured:" + echo " Compiler: $CC" + echo " libc: glibc" + echo " LLVM: $(llvm-config --version 2>/dev/null || echo 'not available')" + + # 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 (Clang + glibc)" + echo "Run 'pg-info' for available commands" + ''; + }; + + # GCC + musl variant (cross-compilation) + muslDevShell = pkgs.mkShell { + name = "postgresql-gcc-musl"; + buildInputs = + (getPostgreSQLDeps true) + ++ [ + pkgs.gcc + flameGraphScript + pgbenchScript + ]; + + shellHook = '' + # Same base configuration as main shell + export HISTFILE=.history + export HISTSIZE=1000000 + export HISTFILESIZE=1000000 + + unset LD_LIBRARY_PATH LD_PRELOAD LIBRARY_PATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH + + export PATH="${pkgs.which}/bin:${pkgs.coreutils}/bin:$PATH" + + # Cross-compilation to musl + export CC="${pkgs.gcc}/bin/gcc" + export CXX="${pkgs.gcc}/bin/g++" + + # Point to musl libraries for linking + export PKG_CONFIG_PATH="${pkgs.pkgsMusl.openssl.dev}/lib/pkgconfig:${pkgs.pkgsMusl.zlib.dev}/lib/pkgconfig:${pkgs.pkgsMusl.icu.dev}/lib/pkgconfig" + export CFLAGS="-ggdb -Og -fno-omit-frame-pointer -DUSE_VALGRIND -D_FORTIFY_SOURCE=1 -I${pkgs.pkgsMusl.stdenv.cc.libc}/include" + export CXXFLAGS="-ggdb -Og -fno-omit-frame-pointer -DUSE_VALGRIND -D_FORTIFY_SOURCE=1 -I${pkgs.pkgsMusl.stdenv.cc.libc}/include" + export LDFLAGS="-L${pkgs.pkgsMusl.stdenv.cc.libc}/lib -static-libgcc" + + # PostgreSQL environment + export PG_SOURCE_DIR="${config.pgSourceDir}" + export PG_BUILD_DIR="${config.pgBuildDir}" + 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) + + export GDBINIT="${gdbConfig}" + export PATH="${flameGraphScript}/bin:${pgbenchScript}/bin:$PATH" + + mkdir -p "$PG_BENCH_DIR" "$PG_FLAME_DIR" + + echo "GCC + musl environment configured" + echo " Compiler: $CC" + echo " LibC: musl (cross-compilation)" + + if [ -f ./pg-aliases.sh ]; then + source ./pg-aliases.sh + fi + + echo "PostgreSQL Development Environment Ready (GCC + musl)" + ''; + }; + + # Clang + musl variant (cross-compilation) + clangMuslDevShell = pkgs.mkShell { + name = "postgresql-clang-musl"; + buildInputs = + (getPostgreSQLDeps true) + ++ [ + llvmPkgs.clang + llvmPkgs.lld + flameGraphScript + pgbenchScript + ]; + + shellHook = let + icon = "f121"; + in '' + export HISTFILE=.history + export HISTSIZE=1000000 + export HISTFILESIZE=1000000 + + unset LD_LIBRARY_PATH LD_PRELOAD LIBRARY_PATH C_INCLUDE_PATH CPLUS_INCLUDE_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)\]" + + # Cross-compilation to musl with clang + export CC="${llvmPkgs.clang}/bin/clang" + export CXX="${llvmPkgs.clang}/bin/clang++" + + # Point to musl libraries for linking + export PKG_CONFIG_PATH="${pkgs.pkgsMusl.openssl.dev}/lib/pkgconfig:${pkgs.pkgsMusl.zlib.dev}/lib/pkgconfig:${pkgs.pkgsMusl.icu.dev}/lib/pkgconfig" + export CFLAGS="--target=x86_64-linux-musl -ggdb -Og -fno-omit-frame-pointer -DUSE_VALGRIND -D_FORTIFY_SOURCE=1 -I${pkgs.pkgsMusl.stdenv.cc.libc}/include" + export CXXFLAGS="--target=x86_64-linux-musl -ggdb -Og -fno-omit-frame-pointer -DUSE_VALGRIND -D_FORTIFY_SOURCE=1 -I${pkgs.pkgsMusl.stdenv.cc.libc}/include" + export LDFLAGS="--target=x86_64-linux-musl -L${pkgs.pkgsMusl.stdenv.cc.libc}/lib -fuse-ld=lld" + + # PostgreSQL environment + export PG_SOURCE_DIR="${config.pgSourceDir}" + export PG_BUILD_DIR="${config.pgBuildDir}" + 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) + + export GDBINIT="${gdbConfig}" + export PATH="${flameGraphScript}/bin:${pgbenchScript}/bin:$PATH" + + mkdir -p "$PG_BENCH_DIR" "$PG_FLAME_DIR" + + echo "Clang + musl environment configured" + echo " Compiler: $CC" + echo " LibC: musl (cross-compilation)" + + if [ -f ./pg-aliases.sh ]; then + source ./pg-aliases.sh + fi + + echo "PostgreSQL Development Environment Ready (Clang + musl)" + ''; + }; +in { + inherit devShell clangDevShell muslDevShell clangMuslDevShell gdbConfig flameGraphScript pgbenchScript; +} diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index ed0c0c2dc9f48..fb0f3886092f4 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -106,6 +106,9 @@ static XLogRecPtr log_heap_new_cid(Relation relation, HeapTuple tup); static HeapTuple ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool key_required, bool *copy); +/* GUC variable */ +bool enable_heap_prune_tracking = false; +HeapPruneStats prune_stats_by_context[6] = {0}; /* * Each tuple lock mode has a corresponding heavyweight lock, and one or two @@ -446,6 +449,11 @@ initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock) scan->rs_ntuples = 0; scan->rs_cindex = 0; + /* Initialize scan-time pruning tracking */ + scan->rs_page_updates = 0; + scan->rs_page_pruned = false; + scan->rs_last_pruned_block = InvalidBlockNumber; + /* * Initialize to ForwardScanDirection because it is most common and * because heap scans go forward before going backward (e.g. CURSORs). @@ -569,7 +577,7 @@ heap_prepare_pagescan(TableScanDesc sscan) /* * Prune and repair fragmentation for the whole page, if possible. */ - heap_page_prune_opt(scan->rs_base.rs_rd, buffer); + heap_page_prune_opt(scan->rs_base.rs_rd, buffer, 0, PRUNE_CONTEXT_PREPARE_PAGESCAN); /* * We must hold share lock on the buffer content while examining tuple @@ -925,6 +933,10 @@ heapgettup(HeapScanDesc scan, Assert(BufferGetBlockNumber(scan->rs_cbuf) == scan->rs_cblock); + /* Reset page tracking for new page */ + scan->rs_page_updates = 0; + scan->rs_page_pruned = false; + LockBuffer(scan->rs_cbuf, BUFFER_LOCK_SHARE); page = heapgettup_start_page(scan, dir, &linesleft, &lineoff); continue_page: @@ -942,7 +954,12 @@ heapgettup(HeapScanDesc scan, ItemId lpp = PageGetItemId(page, lineoff); if (!ItemIdIsNormal(lpp)) + { + /* Track dead line pointers as potential modifications */ + if (ItemIdIsDead(lpp)) + scan->rs_page_updates++; continue; + } tuple->t_data = (HeapTupleHeader) PageGetItem(page, lpp); tuple->t_len = ItemIdGetLength(lpp); @@ -972,10 +989,27 @@ heapgettup(HeapScanDesc scan, } /* - * if we get here, it means we've exhausted the items on this page and - * it's time to move to the next. + * Before moving to next page, check if current page needs scan-time + * pruning. This addresses the issue where multiple updates during a + * scan don't trigger pruning until the next scan. */ LockBuffer(scan->rs_cbuf, BUFFER_LOCK_UNLOCK); + if (scan->rs_page_updates > 0 && + scan->rs_cblock != scan->rs_last_pruned_block && + PageNeedsScanPruning(page, scan->rs_page_updates)) + { + /* Attempt opportunistic pruning */ + heap_page_prune_opt(scan->rs_base.rs_rd, scan->rs_cbuf, 0, PRUNE_CONTEXT_SCAN_OPPORTUNISTIC); + + /* Mark this block as pruned to avoid repeated attempts */ + scan->rs_last_pruned_block = scan->rs_cblock; + scan->rs_page_pruned = true; + } + + /* + * If we get here, it means we've exhausted the items on this page and + * it's time to move to the next. + */ } /* end of scan */ @@ -1042,6 +1076,10 @@ heapgettup_pagemode(HeapScanDesc scan, Assert(BufferGetBlockNumber(scan->rs_cbuf) == scan->rs_cblock); + /* Reset page tracking for new page */ + scan->rs_page_updates = 0; + scan->rs_page_pruned = false; + /* prune the page and determine visible tuple offsets */ heap_prepare_pagescan((TableScanDesc) scan); page = BufferGetPage(scan->rs_cbuf); @@ -1079,6 +1117,15 @@ heapgettup_pagemode(HeapScanDesc scan, } } + /* Before ending scan, check if current page needs scan-time pruning */ + if (BufferIsValid(scan->rs_cbuf) && scan->rs_page_updates > 0 && + scan->rs_cblock != scan->rs_last_pruned_block && + PageNeedsScanPruning(BufferGetPage(scan->rs_cbuf), scan->rs_page_updates)) + { + heap_page_prune_opt(scan->rs_base.rs_rd, scan->rs_cbuf, 0, PRUNE_CONTEXT_SCAN_END); + scan->rs_last_pruned_block = scan->rs_cblock; + } + /* end of scan */ if (BufferIsValid(scan->rs_cbuf)) ReleaseBuffer(scan->rs_cbuf); @@ -2150,14 +2197,11 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, } /* - * XXX Should we set PageSetPrunable on this page ? - * - * The inserting transaction may eventually abort thus making this tuple - * DEAD and hence available for pruning. Though we don't want to optimize - * for aborts, if no other tuple in this page is UPDATEd/DELETEd, the - * aborted tuple will never be pruned until next vacuum is triggered. - * - * If you do add PageSetPrunable here, add it in heap_xlog_insert too. + * If the inserting transaction aborts, this tuple will become DEAD and + * can be pruned during subsequent page accesses rather than waiting for + * the next vacuum cycle. This is beneficial for pages that don't + * experience other modifications (UPDATEs/DELETEs) which would normally + * trigger the prunable marking. */ MarkBufferDirty(buffer); @@ -2193,6 +2237,9 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, xlrec.offnum = ItemPointerGetOffsetNumber(&heaptup->t_self); xlrec.flags = 0; + xlrec.prune_xid = InvalidTransactionId; + if (IsTransactionState() && TransactionIdIsNormal(xid)) + xlrec.prune_xid = xid; if (all_visible_cleared) xlrec.flags |= XLH_INSERT_ALL_VISIBLE_CLEARED; if (options & HEAP_INSERT_SPECULATIVE) @@ -2243,7 +2290,23 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, END_CRIT_SECTION(); - UnlockReleaseBuffer(buffer); + /* + * We marked the page as prunable during insert to enable cleanup of this + * tuple if the inserting transaction aborts. The prune_xid has been set + * to the current transaction ID and logged in the WAL record to ensure + * consistent recovery behavior. + */ + if (IsTransactionState() && TransactionIdIsNormal(xid)) + PageSetPrunable(BufferGetPage(buffer), xid); + + LockBuffer(buffer, BUFFER_LOCK_UNLOCK); + + /* Consider pruning the page if it's getting full */ + if (PageIsFull(BufferGetPage(buffer))) + heap_page_prune_opt(relation, buffer, heaptup->t_len, PRUNE_CONTEXT_INSERT_SPACE_CHECK); + + ReleaseBuffer(buffer); + if (vmbuffer != InvalidBuffer) ReleaseBuffer(vmbuffer); @@ -2520,7 +2583,8 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, PageSetAllVisible(page); /* - * XXX Should we set PageSetPrunable on this page ? See heap_insert() + * Similar to heap_insert() we set the page as prunable and record the + * prune_xid for recovery. */ MarkBufferDirty(buffer); @@ -2563,6 +2627,9 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, Assert(!(all_visible_cleared && all_frozen_set)); xlrec->flags = 0; + xlrec->prune_xid = InvalidTransactionId; + if (IsTransactionState() && TransactionIdIsNormal(xid)) + xlrec->prune_xid = xid; if (all_visible_cleared) xlrec->flags = XLH_INSERT_ALL_VISIBLE_CLEARED; if (all_frozen_set) @@ -2658,7 +2725,21 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, VISIBILITYMAP_ALL_VISIBLE | VISIBILITYMAP_ALL_FROZEN); } - UnlockReleaseBuffer(buffer); + /* + * Similar to heap_insert() we mark the page prunable and have + * recorded prune_xid in the WAL. + */ + if (IsTransactionState() && TransactionIdIsNormal(xid)) + PageSetPrunable(BufferGetPage(buffer), xid); + + LockBuffer(buffer, BUFFER_LOCK_UNLOCK); + + /* Consider pruning the page if it's getting full */ + if (PageIsFull(BufferGetPage(buffer))) + heap_page_prune_opt(relation, buffer, heaptuples[ndone]->t_len, PRUNE_CONTEXT_MULTI_INSERT); + + ReleaseBuffer(buffer); + ndone += nthispage; /* @@ -3819,6 +3900,12 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, newtupsize = MAXALIGN(newtup->t_len); + if (newtupsize > pagefree && PageHasPrunable(page)) + { + heap_page_prune_opt(relation, buffer, newtupsize, PRUNE_CONTEXT_UPDATE_FULL_PAGE); + pagefree = PageGetHeapFreeSpace(page); + } + if (need_toast || newtupsize > pagefree) { TransactionId xmax_lock_old_tuple; diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bcbac844bb669..63b3f131d8ae4 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -138,7 +138,7 @@ heapam_index_fetch_tuple(struct IndexFetchTableData *scan, * Prune page, but only if we weren't already on this page */ if (prev_buf != hscan->xs_cbuf) - heap_page_prune_opt(hscan->xs_base.rel, hscan->xs_cbuf); + heap_page_prune_opt(hscan->xs_base.rel, hscan->xs_cbuf, 0, PRUNE_CONTEXT_SCAN_OPPORTUNISTIC); } /* Obtain share-lock on the buffer so we can examine visibility */ @@ -2517,7 +2517,7 @@ BitmapHeapScanNextBlock(TableScanDesc scan, /* * Prune and repair fragmentation for the whole page, if possible. */ - heap_page_prune_opt(scan->rs_rd, buffer); + heap_page_prune_opt(scan->rs_rd, buffer, 0, PRUNE_CONTEXT_SCAN_OPPORTUNISTIC); /* * We must hold share lock on the buffer content while examining tuple diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.c index cf843277938de..dc928ca34b852 100644 --- a/src/backend/access/heap/heapam_xlog.c +++ b/src/backend/access/heap/heapam_xlog.c @@ -505,6 +505,10 @@ heap_xlog_insert(XLogReaderState *record) freespace = PageGetHeapFreeSpace(page); /* needed to update FSM below */ + if (TransactionIdIsValid(xlrec->prune_xid) && + TransactionIdIsNormal(xlrec->prune_xid)) + PageSetPrunable(page, xlrec->prune_xid); + PageSetLSN(page, lsn); if (xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) @@ -648,6 +652,10 @@ heap_xlog_multi_insert(XLogReaderState *record) freespace = PageGetHeapFreeSpace(page); /* needed to update FSM below */ + if (TransactionIdIsValid(xlrec->prune_xid) && + TransactionIdIsNormal(xlrec->prune_xid)) + PageSetPrunable(page, xlrec->prune_xid); + PageSetLSN(page, lsn); if (xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index d8ea0c78f77cf..6582f24acc544 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -15,6 +15,7 @@ #include "postgres.h" #include "access/heapam.h" +#include "storage/bufpage.h" #include "access/heapam_xlog.h" #include "access/htup_details.h" #include "access/multixact.h" @@ -176,6 +177,7 @@ static void heap_prune_record_unchanged_lp_redirect(PruneState *prstate, OffsetN static void page_verify_redirects(Page page); +extern bool enable_heap_prune_tracking; /* * Optionally prune and repair fragmentation in the specified page. @@ -187,15 +189,45 @@ static void page_verify_redirects(Page page); * Note: this is called quite often. It's important that it fall out quickly * if there's not any use in pruning. * + * If tuple_len is provided (> 0), the function will consider pruning even + * if the page doesn't meet the normal free space threshold, as long as + * pruning could potentially make room for a tuple of that size. + * * Caller must have pin on the buffer, and must *not* have a lock on it. */ void -heap_page_prune_opt(Relation relation, Buffer buffer) +heap_page_prune_opt(Relation relation, Buffer buffer, Size tuple_len, HeapPruneContext context) { Page page = BufferGetPage(buffer); TransactionId prune_xid; GlobalVisState *vistest; Size minfree; + Size freespace_before = 0; + Size freespace_after = 0; + instr_time start_time, + end_time; + uint64 hot_updates = 0; + HeapPruneExitReason exit_reason = HEAP_PRUNE_EXIT_SUCCESS; + + static const char *context_names[] = { + "UPDATE_FULL_PAGE", + "INSERT_SPACE_CHECK", + "SCAN_OPPORTUNISTIC", + "SCAN_END", + "MULTI_INSERT", + "PREPARE_PAGESCAN" + }; + + /* Track statistics if enabled */ + if (enable_heap_prune_tracking) + { + INSTR_TIME_SET_CURRENT(start_time); + prune_stats_by_context[context].calls_total++; + freespace_before = PageGetHeapFreeSpace(page); + /* Get HOT updates on relation */ + if (relation->pgstat_info) + hot_updates = relation->pgstat_info->counts.tuples_hot_updated; + } /* * We can't write WAL in recovery mode, so there's no point trying to @@ -203,7 +235,15 @@ heap_page_prune_opt(Relation relation, Buffer buffer) * soon anyway, so this is no particular loss. */ if (RecoveryInProgress()) + { + if (enable_heap_prune_tracking) + { + exit_reason = HEAP_PRUNE_EXIT_RECOVERY_IN_PROGRESS; + elog(DEBUG2, "heap_page_prune_opt: context=%s RETURNed RecoveryInProgress", context_names[context]); + goto exit_with_reason; + } return; + } /* * First check whether there's any chance there's something to prune, @@ -212,7 +252,15 @@ heap_page_prune_opt(Relation relation, Buffer buffer) */ prune_xid = ((PageHeader) page)->pd_prune_xid; if (!TransactionIdIsValid(prune_xid)) + { + if (enable_heap_prune_tracking) + { + exit_reason = HEAP_PRUNE_EXIT_INVALID_XACT_XID; + elog(DEBUG2, "heap_page_prune_opt: context=%s RETURNed !TransactionIdIsValid", context_names[context]); + goto exit_with_reason; + } return; + } /* * Check whether prune_xid indicates that there may be dead rows that can @@ -221,13 +269,25 @@ heap_page_prune_opt(Relation relation, Buffer buffer) vistest = GlobalVisTestFor(relation); if (!GlobalVisTestIsRemovableXid(vistest, prune_xid)) + { + if (enable_heap_prune_tracking) + { + exit_reason = HEAP_PRUNE_EXIT_NO_REMOVABLE_XIDS; + elog(DEBUG2, "heap_page_prune_opt: context=%s RETURNed !GlobalVisTestIsRemovableXid", context_names[context]); + goto exit_with_reason; + } return; + } /* * We prune when a previous UPDATE failed to find enough space on the page * for a new tuple version, or when free space falls below the relation's * fill-factor target (but not less than 10%). * + * If a specific tuple length is provided, we also consider pruning if the + * current free space plus potential space from pruning could accommodate + * the tuple, even if the page doesn't meet the normal threshold. + * * Checking free space here is questionable since we aren't holding any * lock on the buffer; in the worst case we could get a bogus answer. It's * unlikely to be *seriously* wrong, though, since reading either pd_lower @@ -239,18 +299,32 @@ heap_page_prune_opt(Relation relation, Buffer buffer) HEAP_DEFAULT_FILLFACTOR); minfree = Max(minfree, BLCKSZ / 10); - if (PageIsFull(page) || PageGetHeapFreeSpace(page) < minfree) + /* + * Check if we should prune based on normal criteria or tuple-specific + * needs + */ + if (PageIsFull(page) || PageGetHeapFreeSpace(page) < minfree || tuple_len > 0) { /* OK, try to get exclusive buffer lock */ if (!ConditionalLockBufferForCleanup(buffer)) + { + if (enable_heap_prune_tracking) + { + exit_reason = HEAP_PRUNE_EXIT_LOCK_FAILED; + elog(DEBUG2, "heap_page_prune_opt: RETURNed !ConditionalLockBufferForCleanup"); + goto exit_with_reason; + } return; + } /* * Now that we have buffer lock, get accurate information about the * page's free space, and recheck the heuristic about whether to * prune. */ - if (PageIsFull(page) || PageGetHeapFreeSpace(page) < minfree) + if (PageIsFull(page) || PageGetHeapFreeSpace(page) < minfree || + (tuple_len > 0 && PageGetHeapFreeSpace(page) < MAXALIGN(tuple_len) && + PageHasPrunable(page))) { OffsetNumber dummy_off_loc; PruneFreezeResult presult; @@ -280,6 +354,38 @@ heap_page_prune_opt(Relation relation, Buffer buffer) if (presult.ndeleted > presult.nnewlpdead) pgstat_update_heap_dead_tuples(relation, presult.ndeleted - presult.nnewlpdead); + + + /* Update statistics */ + if (enable_heap_prune_tracking) + { + int tuples_pruned = presult.ndeleted; + + freespace_after = PageGetHeapFreeSpace(page); + + if (tuples_pruned > 0) + { + exit_reason = HEAP_PRUNE_EXIT_SUCCESS; + prune_stats_by_context[context].pages_pruned++; + prune_stats_by_context[context].tuples_pruned += tuples_pruned; + prune_stats_by_context[context].space_freed += (freespace_after - freespace_before); + + elog(DEBUG2, "heap_page_prune_opt: context=%s, tuples_pruned=%d, space_freed=%zu, " + "tuple_len=%zu, hot_updates=%lu, relation=%s", + context_names[context], tuples_pruned, + (freespace_after - freespace_before), tuple_len, + hot_updates, RelationGetRelationName(relation)); + } + else + { + exit_reason = HEAP_PRUNE_EXIT_OTHER; + elog(DEBUG3, "heap_page_prune_opt: context=%s, no pruning done, freespace_before=%zu, " + "tuple_len=%zu, relation=%s", + context_names[context], freespace_before, tuple_len, + RelationGetRelationName(relation)); + } + + } } /* And release buffer lock */ @@ -291,6 +397,52 @@ heap_page_prune_opt(Relation relation, Buffer buffer) * free space should be reused by UPDATEs to *this* page. */ } + + else if (enable_heap_prune_tracking) + { + /* Called but no pruning needed */ + exit_reason = HEAP_PRUNE_EXIT_PAGE_NOT_PRUNABLE; + elog(DEBUG3, "heap_page_prune_opt: context=%s, no pruning needed, freespace=%zu, " + "tuple_len=%zu, relation=%s", + context_names[context], freespace_before, tuple_len, + RelationGetRelationName(relation)); + } + + +exit_with_reason: + if (enable_heap_prune_tracking) + { + /* Record timing */ + INSTR_TIME_SET_CURRENT(end_time); + INSTR_TIME_SUBTRACT(end_time, start_time); + prune_stats_by_context[context].time_spent_us += INSTR_TIME_GET_MICROSEC(end_time); + + /* Record exit reason */ + switch (exit_reason) + { + case HEAP_PRUNE_EXIT_SUCCESS: + prune_stats_by_context[context].exit_success++; + break; + case HEAP_PRUNE_EXIT_RECOVERY_IN_PROGRESS: + prune_stats_by_context[context].exit_recover_in_progress++; + break; + case HEAP_PRUNE_EXIT_INVALID_XACT_XID: + prune_stats_by_context[context].exit_invalid_xact_xid++; + break; + case HEAP_PRUNE_EXIT_NO_REMOVABLE_XIDS: + prune_stats_by_context[context].exit_no_removable_xids++; + break; + case HEAP_PRUNE_EXIT_LOCK_FAILED: + prune_stats_by_context[context].exit_lock_failed++; + break; + case HEAP_PRUNE_EXIT_PAGE_NOT_PRUNABLE: + prune_stats_by_context[context].exit_page_not_prunable++; + break; + default: + prune_stats_by_context[context].exit_other++; + break; + } + } } diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index c756c2bebaaa0..cc89546583096 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -31,6 +31,7 @@ #include "utils/acl.h" #include "utils/builtins.h" #include "utils/timestamp.h" +#include "access/heapam.h" #define UINT32_ACCESS_ONCE(var) ((uint32)(*((volatile uint32 *)&(var)))) @@ -2164,6 +2165,58 @@ pg_stat_get_replication_slot(PG_FUNCTION_ARGS) PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); } +/* + * pg_stat_get_heap_prune_stats - return heap pruning statistics + */ +Datum +pg_stat_get_heap_prune_stats(PG_FUNCTION_ARGS) +{ + TupleDesc tupdesc; + Datum values[12]; + bool nulls[12]; + HeapTuple tuple; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + + static const char *context_names[] = { + "UPDATE_FULL_PAGE", + "INSERT_SPACE_CHECK", + "SCAN_OPPORTUNISTIC", + "SCAN_END", + "MULTI_INSERT", + "PREPARE_PAGESCAN", + }; + + /* Build a tuple descriptor for our result type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + InitMaterializedSRF(fcinfo, MAT_SRF_USE_EXPECTED_DESC); + + for (int i = 0; i < 5; i++) + { + memset(nulls, 0, sizeof(nulls)); + + + values[0] = CStringGetTextDatum(context_names[i]); + values[1] = Int64GetDatum(prune_stats_by_context[i].calls_total); + values[2] = Int64GetDatum(prune_stats_by_context[i].pages_pruned); + values[3] = Int64GetDatum(prune_stats_by_context[i].tuples_pruned); + values[4] = Int64GetDatum(prune_stats_by_context[i].space_freed); + values[5] = Int64GetDatum(prune_stats_by_context[i].time_spent_us); + values[6] = Int64GetDatum(prune_stats_by_context[i].exit_success); + values[7] = Int64GetDatum(prune_stats_by_context[i].exit_invalid_xact_xid); + values[8] = Int64GetDatum(prune_stats_by_context[i].exit_no_removable_xids); + values[9] = Int64GetDatum(prune_stats_by_context[i].exit_page_not_prunable); + values[10] = Int64GetDatum(prune_stats_by_context[i].exit_lock_failed); + values[11] = Int64GetDatum(prune_stats_by_context[i].exit_other); + + tuple = heap_form_tuple(tupdesc, values, nulls); + tuplestore_puttuple(rsinfo->setResult, tuple); + } + + return (Datum) 0; +} + /* * Get the subscription statistics for the given subscription. If the * subscription statistics is not available, return all-zeros stats. diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index 6bc6be13d2ad2..e00b8d2899508 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -3475,4 +3475,10 @@ assign_hook => 'assign_io_method', }, +{ name => 'enable_heap_prune_tracking', type => 'bool', context => 'PGC_USERSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Enables tracking of heap pruning statistics.', + variable => 'enable_heap_prune_tracking', + boot_val => 'true', +}, + ] diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index a1de400b9a531..25993d2234503 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -32,6 +32,47 @@ #include "utils/relcache.h" #include "utils/snapshot.h" +/* Heap pruning statistics */ +typedef enum HeapPruneContext +{ + PRUNE_CONTEXT_UPDATE_FULL_PAGE, /* Update on full page (new) */ + PRUNE_CONTEXT_INSERT_SPACE_CHECK, /* Insert with space check (new) */ + PRUNE_CONTEXT_SCAN_OPPORTUNISTIC, /* Scan-time opportunistic (new) */ + PRUNE_CONTEXT_SCAN_END, /* End of scan cleanup (new) */ + PRUNE_CONTEXT_MULTI_INSERT, /* Multi-insert operation (new) */ + PRUNE_CONTEXT_PREPARE_PAGESCAN /* Prepare scan (pre-existing) */ +} HeapPruneContext; + +typedef enum HeapPruneExitReason +{ + HEAP_PRUNE_EXIT_SUCCESS = 0, + HEAP_PRUNE_EXIT_RECOVERY_IN_PROGRESS, + HEAP_PRUNE_EXIT_INVALID_XACT_XID, + HEAP_PRUNE_EXIT_NO_REMOVABLE_XIDS, + HEAP_PRUNE_EXIT_LOCK_FAILED, + HEAP_PRUNE_EXIT_PAGE_NOT_PRUNABLE, + HEAP_PRUNE_EXIT_OTHER +} HeapPruneExitReason; + +typedef struct HeapPruneStats +{ + uint64 calls_total; /* Total calls to heap_page_prune_opt */ + uint64 pages_pruned; /* Pages that had items pruned */ + uint64 tuples_pruned; /* Total tuples pruned */ + uint64 space_freed; /* Total space freed (bytes) */ + uint64 time_spent_us; /* Total time spent in microseconds */ + + /* Exit reason counters */ + int64 exit_success; + int64 exit_recover_in_progress; + int64 exit_invalid_xact_xid; + int64 exit_no_removable_xids; + int64 exit_lock_failed; + int64 exit_page_not_prunable; + int64 exit_other; +} HeapPruneStats; +extern HeapPruneStats prune_stats_by_context[6]; + /* "options" flag bits for heap_insert */ #define HEAP_INSERT_SKIP_FSM TABLE_INSERT_SKIP_FSM @@ -97,6 +138,12 @@ typedef struct HeapScanDescData uint32 rs_cindex; /* current tuple's index in vistuples */ uint32 rs_ntuples; /* number of visible tuples on page */ OffsetNumber rs_vistuples[MaxHeapTuplesPerPage]; /* their offsets */ + + /* scan-time pruning tracking */ + int rs_page_updates; /* count of updates/deletes on current + * page */ + bool rs_page_pruned; /* whether current page was already pruned */ + BlockNumber rs_last_pruned_block; /* last block we attempted pruning on */ } HeapScanDescData; typedef struct HeapScanDescData *HeapScanDesc; @@ -366,7 +413,8 @@ extern TransactionId heap_index_delete_tuples(Relation rel, /* in heap/pruneheap.c */ struct GlobalVisState; -extern void heap_page_prune_opt(Relation relation, Buffer buffer); +extern void heap_page_prune_opt(Relation relation, Buffer buffer, Size tuple_len, + HeapPruneContext context); extern void heap_page_prune_and_freeze(Relation relation, Buffer buffer, struct GlobalVisState *vistest, int options, diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h index d4c0625b63228..83d69d0b979bd 100644 --- a/src/include/access/heapam_xlog.h +++ b/src/include/access/heapam_xlog.h @@ -159,6 +159,7 @@ typedef struct xl_heap_header /* This is what we need to know about insert */ typedef struct xl_heap_insert { + TransactionId prune_xid; /* record xid for pruning purposes */ OffsetNumber offnum; /* inserted tuple's offset */ uint8 flags; @@ -180,6 +181,7 @@ typedef struct xl_heap_insert */ typedef struct xl_heap_multi_insert { + TransactionId prune_xid; /* record xid for pruning purposes */ uint8 flags; uint16 ntuples; OffsetNumber offsets[FLEXIBLE_ARRAY_MEMBER]; diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h index 77eb41eb6dc99..ced445414491b 100644 --- a/src/include/access/tableam.h +++ b/src/include/access/tableam.h @@ -32,7 +32,7 @@ /* GUCs */ extern PGDLLIMPORT char *default_table_access_method; extern PGDLLIMPORT bool synchronize_seqscans; - +extern PGDLLIMPORT bool enable_heap_prune_tracking; struct BulkInsertStateData; struct IndexInfo; diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 01eba3b5a1909..f19b9abf362a2 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12588,4 +12588,9 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +{ oid => '9999', descr => 'statistics for heap pruning operations', + proname => 'pg_stat_get_heap_prune_stats', prorows => '5', proisstrict => 'f', + proretset => 't', provolatile => 's', proparallel => 'r', + prorettype => 'record', proargtypes => '', + prosrc => 'pg_stat_get_heap_prune_stats' }, ] diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h index aeb67c498c59f..b4a4504c5643f 100644 --- a/src/include/storage/bufpage.h +++ b/src/include/storage/bufpage.h @@ -14,6 +14,7 @@ #ifndef BUFPAGE_H #define BUFPAGE_H +#include "access/transam.h" #include "access/xlogdefs.h" #include "storage/block.h" #include "storage/item.h" @@ -404,6 +405,7 @@ PageSetHasFreeLinePointers(Page page) { ((PageHeader) page)->pd_flags |= PD_HAS_FREE_LINES; } + static inline void PageClearHasFreeLinePointers(Page page) { @@ -415,6 +417,7 @@ PageIsFull(const PageData *page) { return ((const PageHeaderData *) page)->pd_flags & PD_PAGE_FULL; } + static inline void PageSetFull(Page page) { @@ -508,4 +511,18 @@ extern bool PageIndexTupleOverwrite(Page page, OffsetNumber offnum, extern char *PageSetChecksumCopy(Page page, BlockNumber blkno); extern void PageSetChecksumInplace(Page page, BlockNumber blkno); +static inline bool +PageHasPrunable(const PageData *page) +{ + return (PageHasFreeLinePointers(page) || + PageGetFreeSpace(page) < BLCKSZ / 4 || + PageGetMaxOffsetNumber(page) > 21); +} + +static inline bool +PageNeedsScanPruning(const PageData *page, int modifications) +{ + return modifications >= 3 && PageHasPrunable(page); +} + #endif /* BUFPAGE_H */ diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h index ce6285a2c0376..03bddf2d23606 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -136,4 +136,6 @@ extern int32 type_maximum_size(Oid type_oid, int32 typemod); /* quote.c */ extern char *quote_literal_cstr(const char *rawstr); +extern Datum pg_stat_get_heap_prune_stats(PG_FUNCTION_ARGS); + #endif /* BUILTINS_H */ diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c index 61c035a39834a..6661559b684c6 100644 --- a/src/test/regress/pg_regress.c +++ b/src/test/regress/pg_regress.c @@ -1232,7 +1232,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/tools/pgindent/pgindent b/src/tools/pgindent/pgindent index b7d718089248e..e1febc2c7b295 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-2025, PostgreSQL Global Development Group diff --git a/test.log b/test.log new file mode 100644 index 0000000000000..388ac179b92b2 --- /dev/null +++ b/test.log @@ -0,0 +1,870 @@ +/******** QUERY *********/ +SET enable_heap_prune_tracking = on; +/************************/ + +SET +/******** QUERY *********/ +SET log_min_messages = debug2; +/************************/ + +SET +/******** QUERY *********/ +ALTER SYSTEM SET autovacuum = off; +/************************/ + +ALTER SYSTEM +/******** QUERY *********/ +SELECT pg_reload_conf(); +/************************/ + + pg_reload_conf +---------------- + t +(1 row) + +/******** QUERY *********/ +CREATE EXTENSION IF NOT EXISTS pageinspect; +/************************/ + +CREATE EXTENSION +/******** QUERY *********/ +DROP FUNCTION IF EXISTS capture_prune_stats(); +/************************/ + +DROP FUNCTION +/******** QUERY *********/ +DROP FUNCTION IF EXISTS analyze_exit_reasons(); +/************************/ + +DROP FUNCTION +/******** QUERY *********/ +CREATE OR REPLACE FUNCTION capture_prune_stats() +RETURNS TABLE( + context text, + calls_total bigint, + pages_pruned bigint, + tuples_pruned bigint, + space_freed bigint, + time_spent_us bigint, + exit_success bigint, + exit_invalid_xact_xid bigint, + exit_no_removable_xids bigint, + exit_page_not_prunable bigint, + exit_lock_failed bigint, + exit_other bigint, + prune_success_rate_pct numeric, + avg_time_per_call_us numeric +) AS $$ +BEGIN + RETURN QUERY + SELECT + s.context, + s.calls_total, + s.pages_pruned, + s.tuples_pruned, + s.space_freed, + s.time_spent_us, + s.exit_success, + s.exit_invalid_xact_xid, + s.exit_no_removable_xids, + s.exit_page_not_prunable, + s.exit_lock_failed, + s.exit_other, + CASE WHEN s.calls_total > 0 + THEN round(100.0 * s.pages_pruned / s.calls_total, 2) + ELSE 0 + END as prune_success_rate_pct, + CASE WHEN s.calls_total > 0 + THEN round(s.time_spent_us::numeric / s.calls_total, 3) + ELSE 0 + END as avg_time_per_call_us + FROM pg_stat_get_heap_prune_stats() AS s( + context text, + calls_total bigint, + pages_pruned bigint, + tuples_pruned bigint, + space_freed bigint, + time_spent_us bigint, + exit_success bigint, + exit_invalid_xact_xid bigint, + exit_no_removable_xids bigint, + exit_page_not_prunable bigint, + exit_lock_failed bigint, + exit_other bigint + ) + ORDER BY s.calls_total DESC; +END; +$$ LANGUAGE plpgsql; +/************************/ + +CREATE FUNCTION +/******** QUERY *********/ +CREATE OR REPLACE FUNCTION analyze_exit_reasons() +RETURNS TABLE( + context text, + calls_total bigint, + success_rate_pct numeric, + main_failure_reason text, + failure_count bigint, + failure_pct numeric +) AS $$ +BEGIN + RETURN QUERY + WITH exit_analysis AS ( + SELECT + s.context as ctx, + s.calls_total as total_calls, + s.exit_success, + s.exit_invalid_xact_xid, + s.exit_no_removable_xids, + s.exit_page_not_prunable, + s.exit_lock_failed, + s.exit_other, + CASE WHEN s.calls_total > 0 + THEN round(100.0 * s.exit_success / s.calls_total, 2) + ELSE 0 + END as success_rate + FROM capture_prune_stats() s + WHERE s.calls_total > 0 + ), + failure_reasons AS ( + SELECT + ea.ctx, + ea.total_calls, + ea.success_rate, + CASE + WHEN ea.exit_invalid_xact_xid >= GREATEST(ea.exit_no_removable_xids, ea.exit_page_not_prunable, ea.exit_lock_failed, ea.exit_other) + THEN 'INVALID_XACT_XID' + WHEN ea.exit_no_removable_xids >= GREATEST(ea.exit_invalid_xact_xid, ea.exit_page_not_prunable, ea.exit_lock_failed, ea.exit_other) + THEN 'NO_REMOVABLE_XIDS' + WHEN ea.exit_page_not_prunable >= GREATEST(ea.exit_invalid_xact_xid, ea.exit_no_removable_xids, ea.exit_lock_failed, ea.exit_other) + THEN 'PAGE_NOT_PRUNABLE' + WHEN ea.exit_lock_failed >= GREATEST(ea.exit_invalid_xact_xid, ea.exit_no_removable_xids, ea.exit_page_not_prunable, ea.exit_other) + THEN 'LOCK_FAILED' + ELSE 'OTHER' + END as main_reason, + GREATEST(ea.exit_invalid_xact_xid, ea.exit_no_removable_xids, ea.exit_page_not_prunable, ea.exit_lock_failed, ea.exit_other) as max_failure_count + FROM exit_analysis ea + ) + SELECT + fr.ctx, + fr.total_calls, + fr.success_rate, + fr.main_reason, + fr.max_failure_count, + CASE WHEN fr.total_calls > 0 + THEN round(100.0 * fr.max_failure_count / fr.total_calls, 2) + ELSE 0 + END as failure_percentage + FROM failure_reasons fr + ORDER BY fr.total_calls DESC; +END; +$$ LANGUAGE plpgsql; +/************************/ + +CREATE FUNCTION +/******** QUERY *********/ +SELECT pg_stat_reset(); +/************************/ + + pg_stat_reset +--------------- + +(1 row) + +/******** QUERY *********/ +CREATE TABLE ultimate_prune_test ( + id serial PRIMARY KEY, + data text, + status varchar(20) DEFAULT 'active', + counter bigint DEFAULT 0, + -- Make tuples absolutely massive - ~6KB each + mega_padding1 text DEFAULT repeat('AAAAAAAA', 750), -- 6KB + mega_padding2 text DEFAULT repeat('BBBBBBBB', 750), -- 6KB + mega_padding3 text DEFAULT repeat('CCCCCCCC', 750) -- 6KB +); +/************************/ + +CREATE TABLE +/******** QUERY *********/ +ALTER TABLE ultimate_prune_test SET (autovacuum_enabled = false); +/************************/ + +ALTER TABLE +/******** QUERY *********/ +CREATE INDEX idx_ultimate_status ON ultimate_prune_test(status); +/************************/ + +CREATE INDEX +/******** QUERY *********/ +INSERT INTO ultimate_prune_test (data, mega_padding1, mega_padding2, mega_padding3) +SELECT + 'initial_' || i, + repeat('INIT1_' || i, 750), + repeat('INIT2_' || i, 750), + repeat('INIT3_' || i, 750) +FROM generate_series(1, 20) i; +/************************/ + +INSERT 0 20 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +SELECT 'BASELINE' as phase, * FROM capture_prune_stats(); +/************************/ + + phase | context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +----------+--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + BASELINE | SCAN_OPPORTUNISTIC | 506 | 4 | 79 | 15472 | 3906 | 4 | 364 | 53 | 85 | 0 | 0 | 0.79 | 7.719 + BASELINE | UPDATE_FULL_PAGE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + BASELINE | INSERT_SPACE_CHECK | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + BASELINE | SCAN_END | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 + BASELINE | MULTI_INSERT | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 +(5 rows) + +/******** QUERY *********/ +BEGIN; +/************************/ + +BEGIN +/******** QUERY *********/ +UPDATE ultimate_prune_test SET + data = 'dead_v1_' || id, + counter = counter + 1000, + mega_padding1 = repeat('DEAD1_V1_' || id, 750), + mega_padding2 = repeat('DEAD2_V1_' || id, 750), + mega_padding3 = repeat('DEAD3_V1_' || id, 750) +WHERE id BETWEEN 1 AND 10; +/************************/ + +UPDATE 10 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +/************************/ + + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 525 | 4 | 79 | 15472 | 3943 | 4 | 376 | 53 | 86 | 6 | 0 | 0.76 | 7.510 + UPDATE_FULL_PAGE | 10 | 0 | 0 | 0 | 15 | 0 | 0 | 0 | 0 | 10 | 0 | 0.00 | 1.500 +(2 rows) + +/******** QUERY *********/ +SELECT * FROM analyze_exit_reasons(); +/************************/ + + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 531 | 0.75 | INVALID_XACT_XID | 380 | 71.56 + UPDATE_FULL_PAGE | 10 | 0.00 | LOCK_FAILED | 10 | 100.00 +(2 rows) + +/******** QUERY *********/ +BEGIN; +/************************/ + +BEGIN +/******** QUERY *********/ +UPDATE ultimate_prune_test SET + data = 'dead_v2_' || id, + counter = counter + 2000, + mega_padding1 = repeat('DEAD1_V2_' || id, 750), + mega_padding2 = repeat('DEAD2_V2_' || id, 750), + mega_padding3 = repeat('DEAD3_V2_' || id, 750) +WHERE id BETWEEN 11 AND 20; +/************************/ + +UPDATE 10 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +/************************/ + + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 535 | 5 | 89 | 18456 | 4075 | 5 | 380 | 56 | 88 | 6 | 0 | 0.93 | 7.617 + UPDATE_FULL_PAGE | 20 | 0 | 0 | 0 | 29 | 0 | 1 | 9 | 0 | 10 | 0 | 0.00 | 1.450 +(2 rows) + +/******** QUERY *********/ +SELECT * FROM analyze_exit_reasons(); +/************************/ + + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 535 | 0.93 | INVALID_XACT_XID | 380 | 71.03 + UPDATE_FULL_PAGE | 20 | 0.00 | LOCK_FAILED | 10 | 50.00 +(2 rows) + +/******** QUERY *********/ +VACUUM (ANALYZE, VERBOSE) ultimate_prune_test; +/************************/ + +VACUUM +/******** QUERY *********/ +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +/************************/ + + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 636 | 5 | 89 | 18456 | 4147 | 5 | 440 | 56 | 129 | 6 | 0 | 0.79 | 6.520 + UPDATE_FULL_PAGE | 20 | 0 | 0 | 0 | 29 | 0 | 1 | 9 | 0 | 10 | 0 | 0.00 | 1.450 +(2 rows) + +/******** QUERY *********/ +SELECT * FROM analyze_exit_reasons(); +/************************/ + + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 636 | 0.79 | INVALID_XACT_XID | 440 | 69.18 + UPDATE_FULL_PAGE | 20 | 0.00 | LOCK_FAILED | 10 | 50.00 +(2 rows) + +/******** QUERY *********/ +CREATE TABLE insert_mega_test ( + id serial, + huge_data text DEFAULT repeat('XXXXXXXX', 1500) -- 12KB per tuple +); +/************************/ + +CREATE TABLE +/******** QUERY *********/ +ALTER TABLE insert_mega_test SET (autovacuum_enabled = false); +/************************/ + +ALTER TABLE +/******** QUERY *********/ +INSERT INTO insert_mega_test (huge_data) +VALUES (repeat('HUGE_INITIAL', 1500)); +/************************/ + +INSERT 0 1 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +BEGIN; +/************************/ + +BEGIN +/******** QUERY *********/ +UPDATE insert_mega_test SET huge_data = repeat('DEAD_HUGE', 1500); +/************************/ + +UPDATE 1 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +VACUUM (ANALYZE, VERBOSE) insert_mega_test; +/************************/ + +VACUUM +/******** QUERY *********/ +INSERT INTO insert_mega_test (huge_data) +VALUES (repeat('TRIGGER_INSERT_PRUNING', 1500)); +/************************/ + +INSERT 0 1 +/******** QUERY *********/ +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +/************************/ + + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 734 | 8 | 144 | 22176 | 4217 | 8 | 450 | 72 | 198 | 6 | 0 | 1.09 | 5.745 + UPDATE_FULL_PAGE | 20 | 0 | 0 | 0 | 29 | 0 | 1 | 9 | 0 | 10 | 0 | 0.00 | 1.450 +(2 rows) + +/******** QUERY *********/ +SELECT * FROM analyze_exit_reasons(); +/************************/ + + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 734 | 1.09 | INVALID_XACT_XID | 450 | 61.31 + UPDATE_FULL_PAGE | 20 | 0.00 | LOCK_FAILED | 10 | 50.00 +(2 rows) + +/******** QUERY *********/ +DROP TABLE insert_mega_test; +/************************/ + +DROP TABLE +/******** QUERY *********/ +BEGIN; +/************************/ + +BEGIN +/******** QUERY *********/ +UPDATE ultimate_prune_test SET + data = 'more_dead_' || id, + counter = counter + 10000, + mega_padding1 = repeat('MOREDEAD1_' || id, 800), + mega_padding2 = repeat('MOREDEAD2_' || id, 800), + mega_padding3 = repeat('MOREDEAD3_' || id, 800) +WHERE id BETWEEN 1 AND 10; +/************************/ + +UPDATE 10 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +VACUUM (ANALYZE, VERBOSE) ultimate_prune_test; +/************************/ + +VACUUM +/******** QUERY *********/ +BEGIN; +/************************/ + +BEGIN +/******** QUERY *********/ +UPDATE ultimate_prune_test SET + data = 'GIGANTIC_UPDATE_' || repeat('X', 500) || '_' || id, + counter = counter + 100000, + mega_padding1 = repeat('GIGANTIC1_' || id, 1000), + mega_padding2 = repeat('GIGANTIC2_' || id, 1000), + mega_padding3 = repeat('GIGANTIC3_' || id, 1000) +WHERE id BETWEEN 1 AND 5; +/************************/ + +UPDATE 5 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +/************************/ + + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 824 | 9 | 162 | 24640 | 4251 | 9 | 461 | 77 | 271 | 6 | 0 | 1.09 | 5.159 + UPDATE_FULL_PAGE | 28 | 0 | 0 | 0 | 43 | 0 | 3 | 15 | 0 | 10 | 0 | 0.00 | 1.536 +(2 rows) + +/******** QUERY *********/ +SELECT * FROM analyze_exit_reasons(); +/************************/ + + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 824 | 1.09 | INVALID_XACT_XID | 461 | 55.95 + UPDATE_FULL_PAGE | 28 | 0.00 | NO_REMOVABLE_XIDS | 15 | 53.57 +(2 rows) + +/******** QUERY *********/ +BEGIN; +/************************/ + +BEGIN +/******** QUERY *********/ +UPDATE ultimate_prune_test SET + data = 'scan_dead_' || id, + counter = counter + 1000000, + mega_padding1 = repeat('SCANDEAD1_' || id, 900), + mega_padding2 = repeat('SCANDEAD2_' || id, 900), + mega_padding3 = repeat('SCANDEAD3_' || id, 900) +WHERE id BETWEEN 11 AND 20; +/************************/ + +UPDATE 10 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +VACUUM (ANALYZE, VERBOSE) ultimate_prune_test; +/************************/ + +VACUUM +/******** QUERY *********/ +SELECT count(*) FROM ultimate_prune_test WHERE data LIKE 'scan_dead_%'; +/************************/ + + count +------- + 10 +(1 row) + +/******** QUERY *********/ +SELECT count(*) FROM ultimate_prune_test WHERE counter > 500000; +/************************/ + + count +------- + 10 +(1 row) + +/******** QUERY *********/ +SELECT avg(length(mega_padding1)) FROM ultimate_prune_test; +/************************/ + + avg +------------------------ + 10390.0000000000000000 +(1 row) + +/******** QUERY *********/ +SET enable_indexscan = off; +/************************/ + +SET +/******** QUERY *********/ +SET enable_bitmapscan = off; +/************************/ + +SET +/******** QUERY *********/ +SELECT count(*) FROM ultimate_prune_test WHERE status = 'active'; +/************************/ + + count +------- + 20 +(1 row) + +/******** QUERY *********/ +SELECT count(*) FROM ultimate_prune_test WHERE length(data) > 50; +/************************/ + + count +------- + 5 +(1 row) + +/******** QUERY *********/ +RESET enable_indexscan; +/************************/ + +RESET +/******** QUERY *********/ +RESET enable_bitmapscan; +/************************/ + +RESET +/******** QUERY *********/ +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +/************************/ + + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 962 | 10 | 194 | 30552 | 4432 | 10 | 567 | 81 | 298 | 6 | 0 | 1.04 | 4.607 + UPDATE_FULL_PAGE | 39 | 0 | 0 | 0 | 71 | 0 | 5 | 23 | 0 | 11 | 0 | 0.00 | 1.821 +(2 rows) + +/******** QUERY *********/ +SELECT * FROM analyze_exit_reasons(); +/************************/ + + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 962 | 1.04 | INVALID_XACT_XID | 567 | 58.94 + UPDATE_FULL_PAGE | 39 | 0.00 | NO_REMOVABLE_XIDS | 23 | 58.97 +(2 rows) + +/******** QUERY *********/ +INSERT INTO ultimate_prune_test (data, mega_padding1, mega_padding2, mega_padding3) +SELECT + 'bulk_mega_' || i, + repeat('BULKMEGA1_' || i, 900), + repeat('BULKMEGA2_' || i, 900), + repeat('BULKMEGA3_' || i, 900) +FROM generate_series(21, 40) i; +/************************/ + +INSERT 0 20 +/******** QUERY *********/ +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +/************************/ + + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 962 | 10 | 194 | 30552 | 4432 | 10 | 567 | 81 | 298 | 6 | 0 | 1.04 | 4.607 + UPDATE_FULL_PAGE | 39 | 0 | 0 | 0 | 71 | 0 | 5 | 23 | 0 | 11 | 0 | 0.00 | 1.821 +(2 rows) + +/******** QUERY *********/ +SELECT * FROM analyze_exit_reasons(); +/************************/ + + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 962 | 1.04 | INVALID_XACT_XID | 567 | 58.94 + UPDATE_FULL_PAGE | 39 | 0.00 | NO_REMOVABLE_XIDS | 23 | 58.97 +(2 rows) + +/******** QUERY *********/ +BEGIN; +/************************/ + +BEGIN +/******** QUERY *********/ +UPDATE ultimate_prune_test SET + data = 'stress1_' || id, + counter = counter + 10000000, + mega_padding1 = repeat('STRESS1A_' || id, 1200), + mega_padding2 = repeat('STRESS1B_' || id, 1200), + mega_padding3 = repeat('STRESS1C_' || id, 1200) +WHERE id BETWEEN 1 AND 20; +/************************/ + +UPDATE 20 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +BEGIN; +/************************/ + +BEGIN +/******** QUERY *********/ +UPDATE ultimate_prune_test SET + data = 'stress2_' || id, + counter = counter + 20000000, + mega_padding1 = repeat('STRESS2A_' || id, 1200), + mega_padding2 = repeat('STRESS2B_' || id, 1200), + mega_padding3 = repeat('STRESS2C_' || id, 1200) +WHERE id BETWEEN 21 AND 40; +/************************/ + +UPDATE 20 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +INSERT INTO ultimate_prune_test (data, mega_padding1, mega_padding2, mega_padding3) +SELECT + 'stress_insert_' || i, + repeat('STRESSINS1_' || i, 1200), + repeat('STRESSINS2_' || i, 1200), + repeat('STRESSINS3_' || i, 1200) +FROM generate_series(41, 60) i; +/************************/ + +INSERT 0 20 +/******** QUERY *********/ +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +/************************/ + + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 999 | 11 | 195 | 31056 | 4490 | 11 | 569 | 113 | 299 | 6 | 1 | 1.10 | 4.494 + UPDATE_FULL_PAGE | 79 | 0 | 0 | 0 | 121 | 0 | 11 | 57 | 0 | 11 | 0 | 0.00 | 1.532 +(2 rows) + +/******** QUERY *********/ +SELECT * FROM analyze_exit_reasons(); +/************************/ + + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 999 | 1.10 | INVALID_XACT_XID | 569 | 56.96 + UPDATE_FULL_PAGE | 79 | 0.00 | NO_REMOVABLE_XIDS | 57 | 72.15 +(2 rows) + +/******** QUERY *********/ +CREATE TABLE force_pruning_test ( + id serial, + data text DEFAULT repeat('Z', 8000) -- 8KB per tuple, 1 per page +); +/************************/ + +CREATE TABLE +/******** QUERY *********/ +ALTER TABLE force_pruning_test SET (autovacuum_enabled = false); +/************************/ + +ALTER TABLE +/******** QUERY *********/ +INSERT INTO force_pruning_test (data) VALUES (repeat('FIRST', 2000)); +/************************/ + +INSERT 0 1 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +BEGIN; +/************************/ + +BEGIN +/******** QUERY *********/ +UPDATE force_pruning_test SET data = repeat('DEAD_FIRST', 2000) WHERE id = 1; +/************************/ + +UPDATE 1 +/******** QUERY *********/ +COMMIT; +/************************/ + +COMMIT +/******** QUERY *********/ +VACUUM (ANALYZE, VERBOSE) force_pruning_test; +/************************/ + +VACUUM +/******** QUERY *********/ +INSERT INTO force_pruning_test (data) VALUES (repeat('MUST_TRIGGER_PRUNING', 2000)); +/************************/ + +INSERT 0 1 +/******** QUERY *********/ +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +/************************/ + + context | calls_total | pages_pruned | tuples_pruned | space_freed | time_spent_us | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct | avg_time_per_call_us +--------------------+-------------+--------------+---------------+-------------+---------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------+---------------------- + SCAN_OPPORTUNISTIC | 1102 | 11 | 195 | 31056 | 4500 | 11 | 574 | 113 | 397 | 6 | 1 | 1.00 | 4.083 + UPDATE_FULL_PAGE | 79 | 0 | 0 | 0 | 121 | 0 | 11 | 57 | 0 | 11 | 0 | 0.00 | 1.532 +(2 rows) + +/******** QUERY *********/ +SELECT * FROM analyze_exit_reasons(); +/************************/ + + context | calls_total | success_rate_pct | main_failure_reason | failure_count | failure_pct +--------------------+-------------+------------------+---------------------+---------------+------------- + SCAN_OPPORTUNISTIC | 1102 | 1.00 | INVALID_XACT_XID | 574 | 52.09 + UPDATE_FULL_PAGE | 79 | 0.00 | NO_REMOVABLE_XIDS | 57 | 72.15 +(2 rows) + +/******** QUERY *********/ +DROP TABLE force_pruning_test; +/************************/ + +DROP TABLE +/******** QUERY *********/ +SELECT + context, + calls_total, + pages_pruned, + tuples_pruned, + space_freed, + exit_success, + exit_invalid_xact_xid, + exit_no_removable_xids, + exit_page_not_prunable, + exit_lock_failed, + exit_other, + prune_success_rate_pct +FROM capture_prune_stats() +WHERE calls_total > 0 +ORDER BY calls_total DESC; +/************************/ + + context | calls_total | pages_pruned | tuples_pruned | space_freed | exit_success | exit_invalid_xact_xid | exit_no_removable_xids | exit_page_not_prunable | exit_lock_failed | exit_other | prune_success_rate_pct +--------------------+-------------+--------------+---------------+-------------+--------------+-----------------------+------------------------+------------------------+------------------+------------+------------------------ + SCAN_OPPORTUNISTIC | 1149 | 11 | 195 | 31056 | 11 | 574 | 113 | 444 | 6 | 1 | 0.96 + UPDATE_FULL_PAGE | 79 | 0 | 0 | 0 | 0 | 11 | 57 | 0 | 11 | 0 | 0.00 +(2 rows) + +/******** QUERY *********/ +SELECT + 'EXIT_REASON_SUMMARY' as report_type, + context, + main_failure_reason, + failure_count, + failure_pct, + success_rate_pct +FROM analyze_exit_reasons() +ORDER BY calls_total DESC; +/************************/ + + report_type | context | main_failure_reason | failure_count | failure_pct | success_rate_pct +---------------------+--------------------+---------------------+---------------+-------------+------------------ + EXIT_REASON_SUMMARY | SCAN_OPPORTUNISTIC | INVALID_XACT_XID | 574 | 49.96 | 0.96 + EXIT_REASON_SUMMARY | UPDATE_FULL_PAGE | NO_REMOVABLE_XIDS | 57 | 72.15 | 0.00 +(2 rows) + +/******** QUERY *********/ +WITH recommendations AS ( + SELECT + context, + main_failure_reason, + CASE main_failure_reason + WHEN 'INVALID_XACT_XID' THEN 'Check prune_xid setting and page header' + WHEN 'NO_REMOVABLE_XIDS' THEN 'Need more transaction churn or longer waits for visibility' + WHEN 'PAGE_NOT_PRUNABLE' THEN 'Check PageHasPrunable() logic and page conditions' + WHEN 'LOCK_FAILED' THEN 'Increase lock acquisition attempts or use different strategy' + ELSE 'Investigate other failure causes' + END as recommendation + FROM analyze_exit_reasons() + WHERE calls_total > 0 +) +SELECT + 'RECOMMENDATIONS' as report_type, + context, + main_failure_reason, + recommendation +FROM recommendations; +/************************/ + + report_type | context | main_failure_reason | recommendation +-----------------+--------------------+---------------------+------------------------------------------------------------ + RECOMMENDATIONS | SCAN_OPPORTUNISTIC | INVALID_XACT_XID | Check prune_xid setting and page header + RECOMMENDATIONS | UPDATE_FULL_PAGE | NO_REMOVABLE_XIDS | Need more transaction churn or longer waits for visibility +(2 rows) + +/******** QUERY *********/ +SELECT + schemaname, relname, n_tup_ins, n_tup_upd, n_tup_hot_upd, n_dead_tup, + CASE WHEN n_tup_upd > 0 THEN round(100.0 * n_tup_hot_upd / n_tup_upd, 2) ELSE 0 END as hot_update_pct, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as table_size, + pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) as heap_size +FROM pg_stat_user_tables +WHERE relname = 'ultimate_prune_test'; +/************************/ + + schemaname | relname | n_tup_ins | n_tup_upd | n_tup_hot_upd | n_dead_tup | hot_update_pct | table_size | heap_size +------------+---------------------+-----------+-----------+---------------+------------+----------------+------------+----------- + public | ultimate_prune_test | 0 | 0 | 0 | 0 | 0 | 136 kB | 64 kB +(1 row) + +/******** QUERY *********/ +ALTER SYSTEM SET autovacuum = on; +/************************/ + +ALTER SYSTEM +/******** QUERY *********/ +SELECT pg_reload_conf(); +/************************/ + + pg_reload_conf +---------------- + t +(1 row) + +/******** QUERY *********/ +DROP TABLE ultimate_prune_test; +/************************/ + +DROP TABLE diff --git a/test.sql b/test.sql new file mode 100644 index 0000000000000..437dda22be31a --- /dev/null +++ b/test.sql @@ -0,0 +1,455 @@ +-- Enable tracking and detailed logging +SET enable_heap_prune_tracking = on; +SET log_min_messages = debug2; + +-- Disable autovacuum globally +ALTER SYSTEM SET autovacuum = off; +SELECT pg_reload_conf(); + +CREATE EXTENSION IF NOT EXISTS pageinspect; + +-- Drop existing functions +DROP FUNCTION IF EXISTS capture_prune_stats(); +DROP FUNCTION IF EXISTS analyze_exit_reasons(); + +-- Updated function to capture statistics with exit reasons +CREATE OR REPLACE FUNCTION capture_prune_stats() +RETURNS TABLE( + context text, + calls_total bigint, + pages_pruned bigint, + tuples_pruned bigint, + space_freed bigint, + time_spent_us bigint, + exit_success bigint, + exit_invalid_xact_xid bigint, + exit_no_removable_xids bigint, + exit_page_not_prunable bigint, + exit_lock_failed bigint, + exit_other bigint, + prune_success_rate_pct numeric, + avg_time_per_call_us numeric +) AS $$ +BEGIN + RETURN QUERY + SELECT + s.context, + s.calls_total, + s.pages_pruned, + s.tuples_pruned, + s.space_freed, + s.time_spent_us, + s.exit_success, + s.exit_invalid_xact_xid, + s.exit_no_removable_xids, + s.exit_page_not_prunable, + s.exit_lock_failed, + s.exit_other, + CASE WHEN s.calls_total > 0 + THEN round(100.0 * s.pages_pruned / s.calls_total, 2) + ELSE 0 + END as prune_success_rate_pct, + CASE WHEN s.calls_total > 0 + THEN round(s.time_spent_us::numeric / s.calls_total, 3) + ELSE 0 + END as avg_time_per_call_us + FROM pg_stat_get_heap_prune_stats() AS s( + context text, + calls_total bigint, + pages_pruned bigint, + tuples_pruned bigint, + space_freed bigint, + time_spent_us bigint, + exit_success bigint, + exit_invalid_xact_xid bigint, + exit_no_removable_xids bigint, + exit_page_not_prunable bigint, + exit_lock_failed bigint, + exit_other bigint + ) + ORDER BY s.calls_total DESC; +END; +$$ LANGUAGE plpgsql; + +-- Function to analyze exit reasons +CREATE OR REPLACE FUNCTION analyze_exit_reasons() +RETURNS TABLE( + context text, + calls_total bigint, + success_rate_pct numeric, + main_failure_reason text, + failure_count bigint, + failure_pct numeric +) AS $$ +BEGIN + RETURN QUERY + WITH exit_analysis AS ( + SELECT + s.context as ctx, + s.calls_total as total_calls, + s.exit_success, + s.exit_invalid_xact_xid, + s.exit_no_removable_xids, + s.exit_page_not_prunable, + s.exit_lock_failed, + s.exit_other, + CASE WHEN s.calls_total > 0 + THEN round(100.0 * s.exit_success / s.calls_total, 2) + ELSE 0 + END as success_rate + FROM capture_prune_stats() s + WHERE s.calls_total > 0 + ), + failure_reasons AS ( + SELECT + ea.ctx, + ea.total_calls, + ea.success_rate, + CASE + WHEN ea.exit_invalid_xact_xid >= GREATEST(ea.exit_no_removable_xids, ea.exit_page_not_prunable, ea.exit_lock_failed, ea.exit_other) + THEN 'INVALID_XACT_XID' + WHEN ea.exit_no_removable_xids >= GREATEST(ea.exit_invalid_xact_xid, ea.exit_page_not_prunable, ea.exit_lock_failed, ea.exit_other) + THEN 'NO_REMOVABLE_XIDS' + WHEN ea.exit_page_not_prunable >= GREATEST(ea.exit_invalid_xact_xid, ea.exit_no_removable_xids, ea.exit_lock_failed, ea.exit_other) + THEN 'PAGE_NOT_PRUNABLE' + WHEN ea.exit_lock_failed >= GREATEST(ea.exit_invalid_xact_xid, ea.exit_no_removable_xids, ea.exit_page_not_prunable, ea.exit_other) + THEN 'LOCK_FAILED' + ELSE 'OTHER' + END as main_reason, + GREATEST(ea.exit_invalid_xact_xid, ea.exit_no_removable_xids, ea.exit_page_not_prunable, ea.exit_lock_failed, ea.exit_other) as max_failure_count + FROM exit_analysis ea + ) + SELECT + fr.ctx, + fr.total_calls, + fr.success_rate, + fr.main_reason, + fr.max_failure_count, + CASE WHEN fr.total_calls > 0 + THEN round(100.0 * fr.max_failure_count / fr.total_calls, 2) + ELSE 0 + END as failure_percentage + FROM failure_reasons fr + ORDER BY fr.total_calls DESC; +END; +$$ LANGUAGE plpgsql; + +SELECT pg_stat_reset(); + +\echo '=== PHASE 1: Setup with Massive Tuples ===' + +CREATE TABLE ultimate_prune_test ( + id serial PRIMARY KEY, + data text, + status varchar(20) DEFAULT 'active', + counter bigint DEFAULT 0, + -- Make tuples absolutely massive - ~6KB each + mega_padding1 text DEFAULT repeat('AAAAAAAA', 750), -- 6KB + mega_padding2 text DEFAULT repeat('BBBBBBBB', 750), -- 6KB + mega_padding3 text DEFAULT repeat('CCCCCCCC', 750) -- 6KB +); + +ALTER TABLE ultimate_prune_test SET (autovacuum_enabled = false); +CREATE INDEX idx_ultimate_status ON ultimate_prune_test(status); + +-- Insert fewer rows but much larger +INSERT INTO ultimate_prune_test (data, mega_padding1, mega_padding2, mega_padding3) +SELECT + 'initial_' || i, + repeat('INIT1_' || i, 750), + repeat('INIT2_' || i, 750), + repeat('INIT3_' || i, 750) +FROM generate_series(1, 20) i; -- Only 20 rows but each is ~18KB + +COMMIT; + +SELECT 'BASELINE' as phase, * FROM capture_prune_stats(); + +\echo '=== PHASE 2: Create Dead Tuples in Separate Transactions ===' + +-- Transaction 1: Create dead tuples +BEGIN; +UPDATE ultimate_prune_test SET + data = 'dead_v1_' || id, + counter = counter + 1000, + mega_padding1 = repeat('DEAD1_V1_' || id, 750), + mega_padding2 = repeat('DEAD2_V1_' || id, 750), + mega_padding3 = repeat('DEAD3_V1_' || id, 750) +WHERE id BETWEEN 1 AND 10; +COMMIT; + +\echo '--- After First Dead Tuple Creation ---' +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +SELECT * FROM analyze_exit_reasons(); + +-- Transaction 2: More dead tuples +BEGIN; +UPDATE ultimate_prune_test SET + data = 'dead_v2_' || id, + counter = counter + 2000, + mega_padding1 = repeat('DEAD1_V2_' || id, 750), + mega_padding2 = repeat('DEAD2_V2_' || id, 750), + mega_padding3 = repeat('DEAD3_V2_' || id, 750) +WHERE id BETWEEN 11 AND 20; +COMMIT; + +\echo '--- After Second Dead Tuple Creation ---' +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +SELECT * FROM analyze_exit_reasons(); + +-- Force visibility with VACUUM +VACUUM (ANALYZE, VERBOSE) ultimate_prune_test; + +\echo '--- After VACUUM ---' +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +SELECT * FROM analyze_exit_reasons(); + +\echo '=== PHASE 3: Extreme INSERT Test ===' + +CREATE TABLE insert_mega_test ( + id serial, + huge_data text DEFAULT repeat('XXXXXXXX', 1500) -- 12KB per tuple +); + +ALTER TABLE insert_mega_test SET (autovacuum_enabled = false); + +-- Insert only 1 tuple to nearly fill a page +INSERT INTO insert_mega_test (huge_data) +VALUES (repeat('HUGE_INITIAL', 1500)); + +COMMIT; + +-- Create dead tuple +BEGIN; +UPDATE insert_mega_test SET huge_data = repeat('DEAD_HUGE', 1500); +COMMIT; + +-- Force visibility +VACUUM (ANALYZE, VERBOSE) insert_mega_test; + +-- Now insert - should trigger INSERT_SPACE_CHECK +INSERT INTO insert_mega_test (huge_data) +VALUES (repeat('TRIGGER_INSERT_PRUNING', 1500)); + +\echo '--- After INSERT Test ---' +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +SELECT * FROM analyze_exit_reasons(); + +DROP TABLE insert_mega_test; + +\echo '=== PHASE 4: UPDATE Test with Extreme Sizes ===' + +-- Create more dead tuples +BEGIN; +UPDATE ultimate_prune_test SET + data = 'more_dead_' || id, + counter = counter + 10000, + mega_padding1 = repeat('MOREDEAD1_' || id, 800), + mega_padding2 = repeat('MOREDEAD2_' || id, 800), + mega_padding3 = repeat('MOREDEAD3_' || id, 800) +WHERE id BETWEEN 1 AND 10; +COMMIT; + +-- Force visibility +VACUUM (ANALYZE, VERBOSE) ultimate_prune_test; + +-- Large updates that should trigger UPDATE_FULL_PAGE +BEGIN; +UPDATE ultimate_prune_test SET + data = 'GIGANTIC_UPDATE_' || repeat('X', 500) || '_' || id, + counter = counter + 100000, + mega_padding1 = repeat('GIGANTIC1_' || id, 1000), + mega_padding2 = repeat('GIGANTIC2_' || id, 1000), + mega_padding3 = repeat('GIGANTIC3_' || id, 1000) +WHERE id BETWEEN 1 AND 5; +COMMIT; + +\echo '--- After UPDATE Test ---' +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +SELECT * FROM analyze_exit_reasons(); + +\echo '=== PHASE 5: Scan Pressure Test ===' + +-- Create more dead tuples for scanning +BEGIN; +UPDATE ultimate_prune_test SET + data = 'scan_dead_' || id, + counter = counter + 1000000, + mega_padding1 = repeat('SCANDEAD1_' || id, 900), + mega_padding2 = repeat('SCANDEAD2_' || id, 900), + mega_padding3 = repeat('SCANDEAD3_' || id, 900) +WHERE id BETWEEN 11 AND 20; +COMMIT; + +-- Force visibility +VACUUM (ANALYZE, VERBOSE) ultimate_prune_test; + +-- Intensive scanning to trigger SCAN_OPPORTUNISTIC +SELECT count(*) FROM ultimate_prune_test WHERE data LIKE 'scan_dead_%'; +SELECT count(*) FROM ultimate_prune_test WHERE counter > 500000; +SELECT avg(length(mega_padding1)) FROM ultimate_prune_test; + +-- Force sequential scans +SET enable_indexscan = off; +SET enable_bitmapscan = off; +SELECT count(*) FROM ultimate_prune_test WHERE status = 'active'; +SELECT count(*) FROM ultimate_prune_test WHERE length(data) > 50; +RESET enable_indexscan; +RESET enable_bitmapscan; + +\echo '--- After SCAN Test ---' +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +SELECT * FROM analyze_exit_reasons(); + +\echo '=== PHASE 6: Multi-Insert Test ===' + +-- Bulk insert with massive tuples +INSERT INTO ultimate_prune_test (data, mega_padding1, mega_padding2, mega_padding3) +SELECT + 'bulk_mega_' || i, + repeat('BULKMEGA1_' || i, 900), + repeat('BULKMEGA2_' || i, 900), + repeat('BULKMEGA3_' || i, 900) +FROM generate_series(21, 40) i; + +\echo '--- After MULTI_INSERT Test ---' +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +SELECT * FROM analyze_exit_reasons(); + +\echo '=== PHASE 7: Ultimate Stress Test ===' + +-- Create extreme conditions without DO blocks +-- Round 1: Massive updates +BEGIN; +UPDATE ultimate_prune_test SET + data = 'stress1_' || id, + counter = counter + 10000000, + mega_padding1 = repeat('STRESS1A_' || id, 1200), + mega_padding2 = repeat('STRESS1B_' || id, 1200), + mega_padding3 = repeat('STRESS1C_' || id, 1200) +WHERE id BETWEEN 1 AND 20; +COMMIT; + +-- Round 2: More massive updates +BEGIN; +UPDATE ultimate_prune_test SET + data = 'stress2_' || id, + counter = counter + 20000000, + mega_padding1 = repeat('STRESS2A_' || id, 1200), + mega_padding2 = repeat('STRESS2B_' || id, 1200), + mega_padding3 = repeat('STRESS2C_' || id, 1200) +WHERE id BETWEEN 21 AND 40; +COMMIT; + +-- Round 3: Inserts +INSERT INTO ultimate_prune_test (data, mega_padding1, mega_padding2, mega_padding3) +SELECT + 'stress_insert_' || i, + repeat('STRESSINS1_' || i, 1200), + repeat('STRESSINS2_' || i, 1200), + repeat('STRESSINS3_' || i, 1200) +FROM generate_series(41, 60) i; + +\echo '--- After STRESS Test ---' +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +SELECT * FROM analyze_exit_reasons(); + +\echo '=== PHASE 8: Force Extreme Conditions ===' + +-- Create table guaranteed to need pruning +CREATE TABLE force_pruning_test ( + id serial, + data text DEFAULT repeat('Z', 8000) -- 8KB per tuple, 1 per page +); + +ALTER TABLE force_pruning_test SET (autovacuum_enabled = false); + +-- Insert exactly 1 tuple (should fill most of a page) +INSERT INTO force_pruning_test (data) VALUES (repeat('FIRST', 2000)); +COMMIT; + +-- Make it dead +BEGIN; +UPDATE force_pruning_test SET data = repeat('DEAD_FIRST', 2000) WHERE id = 1; +COMMIT; + +-- Force visibility +VACUUM (ANALYZE, VERBOSE) force_pruning_test; + +-- This insert MUST trigger pruning +INSERT INTO force_pruning_test (data) VALUES (repeat('MUST_TRIGGER_PRUNING', 2000)); + +\echo '--- After FORCE PRUNING Test ---' +SELECT * FROM capture_prune_stats() WHERE calls_total > 0; +SELECT * FROM analyze_exit_reasons(); + +DROP TABLE force_pruning_test; + +\echo '=== PHASE 9: Final Comprehensive Analysis ===' + +-- Final statistics with detailed exit reason analysis +SELECT + context, + calls_total, + pages_pruned, + tuples_pruned, + space_freed, + exit_success, + exit_invalid_xact_xid, + exit_no_removable_xids, + exit_page_not_prunable, + exit_lock_failed, + exit_other, + prune_success_rate_pct +FROM capture_prune_stats() +WHERE calls_total > 0 +ORDER BY calls_total DESC; + +-- Exit reason summary +SELECT + 'EXIT_REASON_SUMMARY' as report_type, + context, + main_failure_reason, + failure_count, + failure_pct, + success_rate_pct +FROM analyze_exit_reasons() +ORDER BY calls_total DESC; + +-- Recommendations based on failure patterns +WITH recommendations AS ( + SELECT + context, + main_failure_reason, + CASE main_failure_reason + WHEN 'INVALID_XACT_XID' THEN 'Check prune_xid setting and page header' + WHEN 'NO_REMOVABLE_XIDS' THEN 'Need more transaction churn or longer waits for visibility' + WHEN 'PAGE_NOT_PRUNABLE' THEN 'Check PageHasPrunable() logic and page conditions' + WHEN 'LOCK_FAILED' THEN 'Increase lock acquisition attempts or use different strategy' + ELSE 'Investigate other failure causes' + END as recommendation + FROM analyze_exit_reasons() + WHERE calls_total > 0 +) +SELECT + 'RECOMMENDATIONS' as report_type, + context, + main_failure_reason, + recommendation +FROM recommendations; + +-- Table statistics +SELECT + schemaname, relname, n_tup_ins, n_tup_upd, n_tup_hot_upd, n_dead_tup, + CASE WHEN n_tup_upd > 0 THEN round(100.0 * n_tup_hot_upd / n_tup_upd, 2) ELSE 0 END as hot_update_pct, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as table_size, + pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) as heap_size +FROM pg_stat_user_tables +WHERE relname = 'ultimate_prune_test'; + +\echo '=== Test Complete ===' + +ALTER SYSTEM SET autovacuum = on; +SELECT pg_reload_conf(); + +DROP TABLE ultimate_prune_test;