173 lines
7.1 KiB
PL/PgSQL
173 lines
7.1 KiB
PL/PgSQL
\set ON_ERROR_STOP on
|
|
\pset pager off
|
|
\pset format unaligned
|
|
\pset tuples_only on
|
|
|
|
-- SUPERSEDED EXPLORATORY FIXTURE: every generated row shares one timestamp,
|
|
-- so PostgreSQL posting-list compression understates representative btree
|
|
-- storage cost. Keep this file only as raw historical evidence; do not use it
|
|
-- to accept or reject an index. Use admin_user_order_index_representative.sql.
|
|
|
|
-- A/B the missing ORDER BY index used by GET /api/admin/users. Two temporary
|
|
-- tables keep both variants resident and let samples alternate without DDL
|
|
-- contaminating timings. No persistent state survives the transaction.
|
|
BEGIN;
|
|
|
|
CREATE TEMP TABLE perf_users_no_index (
|
|
id uuid NOT NULL,
|
|
username text,
|
|
email text NOT NULL,
|
|
role_text text NOT NULL,
|
|
storage_quota_bytes bigint NOT NULL,
|
|
storage_used_bytes bigint NOT NULL,
|
|
created_at timestamptz NOT NULL,
|
|
last_login_at timestamptz,
|
|
active boolean NOT NULL,
|
|
oidc_provider text,
|
|
is_external boolean NOT NULL
|
|
);
|
|
|
|
INSERT INTO perf_users_no_index
|
|
SELECT
|
|
gen_random_uuid(),
|
|
'user-' || n,
|
|
'user-' || n || '@example.invalid',
|
|
CASE WHEN n % 20 = 0 THEN 'admin' ELSE 'user' END,
|
|
10737418240,
|
|
n::bigint * 1048576,
|
|
-- One hundred accounts intentionally share each timestamp. The real
|
|
-- default is statement-stable CURRENT_TIMESTAMP, so bulk/JIT creation can
|
|
-- produce ties; `id` must make page boundaries deterministic.
|
|
timestamptz '2026-01-01 00:00:00+00' + make_interval(secs => n / 100),
|
|
timestamptz '2026-01-01 00:00:00+00' + make_interval(secs => n / 2),
|
|
true,
|
|
CASE WHEN n % 3 = 0 THEN 'keycloak' END,
|
|
n % 7 = 0
|
|
FROM generate_series(1, 500000) AS n;
|
|
|
|
CREATE TEMP TABLE perf_users_indexed
|
|
(LIKE perf_users_no_index INCLUDING ALL);
|
|
INSERT INTO perf_users_indexed SELECT * FROM perf_users_no_index;
|
|
CREATE INDEX perf_users_indexed_created_at_id
|
|
ON perf_users_indexed (created_at DESC, id DESC);
|
|
|
|
ANALYZE perf_users_no_index;
|
|
ANALYZE perf_users_indexed;
|
|
|
|
SELECT 'created_at_id_index_bytes|' || pg_relation_size('perf_users_indexed_created_at_id');
|
|
SELECT 'tied_timestamp_groups|' || COUNT(*)
|
|
FROM (
|
|
SELECT created_at FROM perf_users_no_index GROUP BY created_at HAVING COUNT(*) > 1
|
|
) tied;
|
|
SELECT 'stable_order_match|' || (
|
|
ARRAY(
|
|
SELECT id FROM perf_users_no_index
|
|
ORDER BY created_at DESC, id DESC LIMIT 200 OFFSET 49900
|
|
) = ARRAY(
|
|
SELECT id FROM perf_users_indexed
|
|
ORDER BY created_at DESC, id DESC LIMIT 200 OFFSET 49900
|
|
)
|
|
);
|
|
SELECT 'adjacent_page_overlap|' || COUNT(*)
|
|
FROM (
|
|
SELECT id FROM perf_users_indexed
|
|
ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0
|
|
) first_page
|
|
JOIN (
|
|
SELECT id FROM perf_users_indexed
|
|
ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 100
|
|
) second_page USING (id);
|
|
|
|
\o /dev/null
|
|
\timing on
|
|
|
|
-- Warm both table variants and both page depths.
|
|
\echo no_index_warmup_first
|
|
SELECT id, username, email, role_text, storage_quota_bytes, storage_used_bytes,
|
|
last_login_at, active, oidc_provider, is_external
|
|
FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo indexed_warmup_first
|
|
SELECT id, username, email, role_text, storage_quota_bytes, storage_used_bytes,
|
|
last_login_at, active, oidc_provider, is_external
|
|
FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo no_index_warmup_deep
|
|
SELECT id, username, email, role_text, storage_quota_bytes, storage_used_bytes,
|
|
last_login_at, active, oidc_provider, is_external
|
|
FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
\echo indexed_warmup_deep
|
|
SELECT id, username, email, role_text, storage_quota_bytes, storage_used_bytes,
|
|
last_login_at, active, oidc_provider, is_external
|
|
FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
|
|
\echo no_index_first_1
|
|
SELECT * FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo indexed_first_1
|
|
SELECT * FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo indexed_deep_1
|
|
SELECT * FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
\echo no_index_deep_1
|
|
SELECT * FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
|
|
\echo indexed_first_2
|
|
SELECT * FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo no_index_first_2
|
|
SELECT * FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo no_index_deep_2
|
|
SELECT * FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
\echo indexed_deep_2
|
|
SELECT * FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
|
|
\echo no_index_first_3
|
|
SELECT * FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo indexed_first_3
|
|
SELECT * FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo indexed_deep_3
|
|
SELECT * FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
\echo no_index_deep_3
|
|
SELECT * FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
|
|
\echo indexed_first_4
|
|
SELECT * FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo no_index_first_4
|
|
SELECT * FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo no_index_deep_4
|
|
SELECT * FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
\echo indexed_deep_4
|
|
SELECT * FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
|
|
\echo no_index_first_5
|
|
SELECT * FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo indexed_first_5
|
|
SELECT * FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 0;
|
|
\echo indexed_deep_5
|
|
SELECT * FROM perf_users_indexed ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
\echo no_index_deep_5
|
|
SELECT * FROM perf_users_no_index ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET 50000;
|
|
|
|
-- Write-cost gate: an ordering index is not free. Copy the same 10k-row shape
|
|
-- into each variant and report the insertion tax alongside the read win.
|
|
\echo no_index_insert_1
|
|
INSERT INTO perf_users_no_index SELECT * FROM perf_users_no_index LIMIT 10000;
|
|
\echo indexed_insert_1
|
|
INSERT INTO perf_users_indexed SELECT * FROM perf_users_indexed LIMIT 10000;
|
|
\echo indexed_insert_2
|
|
INSERT INTO perf_users_indexed SELECT * FROM perf_users_indexed LIMIT 10000;
|
|
\echo no_index_insert_2
|
|
INSERT INTO perf_users_no_index SELECT * FROM perf_users_no_index LIMIT 10000;
|
|
\echo no_index_insert_3
|
|
INSERT INTO perf_users_no_index SELECT * FROM perf_users_no_index LIMIT 10000;
|
|
\echo indexed_insert_3
|
|
INSERT INTO perf_users_indexed SELECT * FROM perf_users_indexed LIMIT 10000;
|
|
\echo indexed_insert_4
|
|
INSERT INTO perf_users_indexed SELECT * FROM perf_users_indexed LIMIT 10000;
|
|
\echo no_index_insert_4
|
|
INSERT INTO perf_users_no_index SELECT * FROM perf_users_no_index LIMIT 10000;
|
|
\echo no_index_insert_5
|
|
INSERT INTO perf_users_no_index SELECT * FROM perf_users_no_index LIMIT 10000;
|
|
\echo indexed_insert_5
|
|
INSERT INTO perf_users_indexed SELECT * FROM perf_users_indexed LIMIT 10000;
|
|
|
|
\timing off
|
|
\o
|
|
ROLLBACK;
|