Skip to content

Commit 89baa48

Browse files
Import gp_relaccess_stats into gpcontrib from Greenplum (open-gpdb#48) (#1954)
* Import gp_relaccess_stats into gpcontrib from Greenplum * Feat: Adapt gp_relaccess_stats for Cloudberry * Fix: Delete _PG_fini --------- Co-authored-by: Maxim Smyatkin <smyatkinmaxim@gmail.com>
1 parent 7d9a362 commit 89baa48

12 files changed

Lines changed: 1779 additions & 0 deletions

File tree

‎.github/workflows/build-cloudberry.yml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ jobs:
279279
},
280280
"enable_core_check":false
281281
},
282+
{"test":"gpcontrib-gp-relaccess-stats",
283+
"make_configs":["gpcontrib/gp_relaccess_stats:installcheck"],
284+
"extension":"gp_relaccess_stats",
285+
"shared_preload_libraries":"gp_relaccess_stats"
286+
},
282287
{"test":"gpcontrib-gp-stats-collector",
283288
"make_configs":["gpcontrib/gp_stats_collector:installcheck"],
284289
"extension":"gp_stats_collector"
@@ -1576,6 +1581,20 @@ jobs:
15761581
exit 1
15771582
fi
15781583
;;
1584+
gp_relaccess_stats)
1585+
if ! su - gpadmin -c "source ${BUILD_DESTINATION}/cloudberry-env.sh && \
1586+
source ${SRC_DIR}/gpAux/gpdemo/gpdemo-env.sh && \
1587+
gpconfig -c shared_preload_libraries -v 'gp_relaccess_stats' && \
1588+
gpstop -ra && \
1589+
echo 'CREATE EXTENSION IF NOT EXISTS gp_relaccess_stats; \
1590+
SHOW shared_preload_libraries; \
1591+
TABLE pg_extension;' | \
1592+
psql postgres"
1593+
then
1594+
echo "Error creating gp_relaccess_stats extension"
1595+
exit 1
1596+
fi
1597+
;;
15791598
*)
15801599
echo "Unknown extension: ${{ matrix.extension }}"
15811600
exit 1

‎gpcontrib/Makefile‎

100644100755
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ifeq "$(enable_debug_extensions)" "yes"
2323
gp_exttable_fdw \
2424
gp_legacy_string_agg \
2525
gp_relsizes_stats \
26+
gp_relaccess_stats \
2627
gp_replica_check \
2728
gp_toolkit \
2829
pg_hint_plan \
@@ -33,6 +34,7 @@ else
3334
gp_internal_tools \
3435
gp_legacy_string_agg \
3536
gp_relsizes_stats \
37+
gp_relaccess_stats \
3638
gp_exttable_fdw \
3739
gp_toolkit \
3840
pg_hint_plan
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.o
2+
*.so
3+
.vscode
4+
compile_commands.json
5+
results
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
MODULE_big = gp_relaccess_stats
2+
OBJS = ./src/gp_relaccess_stats.o
3+
EXTENSION = gp_relaccess_stats
4+
EXTVERSION = 1.0
5+
DATA = $(wildcard sql/*--*.sql)
6+
REGRESS = gp_relaccess_stats
7+
REGRESS_OPTS = --inputdir=test/
8+
PGFILEDESC = "gp_relaccess_stats - facility to track how and when tables, partitions or views were accessed"
9+
PG_CXXFLAGS += $(COMMON_CPP_FLAGS)
10+
11+
ifdef USE_PGXS
12+
PG_CONFIG = pg_config
13+
PGXS := $(shell $(PG_CONFIG) --pgxs)
14+
include $(PGXS)
15+
else
16+
top_builddir = ../..
17+
include $(top_builddir)/src/Makefile.global
18+
include $(top_srcdir)/contrib/contrib-global.mk
19+
endif
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
20+
# gp_relaccess_stats: Table access monitoring tool for Greenplum
21+
22+
## Features
23+
gp_relaccess_stats is an extension that records access statistics for Greenplum tables and views. Allowing users to see what objects were used, when and by whom. For example, this allows DBAs to find objects that are not used anymore or objects that are being misused.
24+
25+
Features include:
26+
* support of both tables (regular, external or partitioned) and views
27+
* separate tracking of select, insert, update and delete queries
28+
* separate tracking of last read and write timestamps
29+
* tracking of the last user who accessed the object
30+
* per-database configuration
31+
* in-memory stats survive server restarts (but not crashes)
32+
33+
### Supported versions and platforms
34+
For now it is being tested only for GP6 and Linux. Though, there are no apparent reasons why it should not be working on newer GP versions (or even PG with slight code modification) or other OSes.
35+
36+
### Configuration
37+
As this extension does extensive usage of hooks and shared memory, you need to load gp_relaccess_stats.so on start-up:
38+
```
39+
gpconfig -c shared_preload_libraries -v '<old_shared_preload_libraries_contents,>gp_relaccess_stats' && gpstop -ra
40+
```
41+
gp_relaccess_stats configuration parameters:
42+
| **Parameter** | **Type** | **Default** | **Default** |
43+
| ---------------- | --------------- | ------------ | ------------ |
44+
| `gp_relaccess_stats.enabled` | bool | false | Using `gp_relaccess_stats.enabled` you can enable/disable stats collection either globally or for each database separately. The second option is preferred.|
45+
| `gp_relaccess_stats.max_tables` | integer | 65536 | `gp_relaccess_stats.max_tables` is a hard limit on how many tables can be cached in shared memory. Feel free to make this number higher if necessary, as the overhead is only about 160 bytes per table. Note, that stats cache for a specific table is evicted from memory any time you execute `relaccess_stats_update()` or `relaccess_stats_dump()` and new tables can be recorded. If you call these functions often enough, there is no need for high gp_relaccess_stats.max_tables|
46+
| `gp_relaccess_stats.dump_on_overflow` | bool | false | This parameter configures what happens in case `gp_relaccess_stats.max_tables` was not enough. If set to `true`, `relaccess_stats_dump()` will be called implicitly and stats cache will be freed. Otherwice, you will get a WARNING saying that there is no room for new stats. Is this case, stats for some tables will be lost.|
47+
48+
### Usage
49+
The first thing you need to do after `CREATE EXTENSION` and configuring - execute `SELECT relaccess_stats_init();` in a specific database. This function will fill `relaccess_stats` table with empty stats for each table and partition in this database. This is optional, but will come handy when you try to find tables that haven't been used recently, for example.
50+
51+
Then, either manually or with a cron job start executing `select relaccess_stats_update()`. This function takes all stats cached in shared memory and all stats stored in pg_stat dir (e.g, dumps after restarts, or when `max_tables` was exceeded) and upserts them into `relaccess_stats` table.
52+
53+
The `relaccess_stats` table itself looks like this:
54+
| **Column** | **Description** |
55+
| ---------------- | --------------- |
56+
| relid | OID of the relation |
57+
| relname | Name of the relation at last access |
58+
| last_reader_id | OID of user who read the table last |
59+
| last_writer_id | OID of user who wrote the table last |
60+
| last_read | Timestamp of the most recent select |
61+
| last_write | Timestamp of the most recent insert/delete/update/truncate |
62+
| n_select_queries | |
63+
| n_insert_queries | |
64+
| n_update_queries | |
65+
| n_delete_queries | |
66+
| n_truncate_queries | |
67+
68+
**NOTE**: n_*_queries columns count the number of queries executed, not the number of rows read, inserted, deleted or updated.
69+
70+
This table has a view associated with it: `relaccess_stats_root_tables_aggregated`. This view has exactly same columns, however it only shows partitioned tables. To be more specific, it shows aggregated stats for each partitioned table.
71+
For example, if we have 1 insert into `tbl1_prt_1` and 3 inserts into `tbl1_prt_2`, then `select * from relaccess_stats_root_tables_aggregated where relname = 'tbl1'` will show us only root table with n_insert_queries = 4. This view, however, has some limitations. See the next section for more detail.
72+
73+
Another useful function is `relaccess_stats_dump()`, which simply moves cached stats from shared memory to temporary files in pg_stat directory. This function is cheaper than `relaccess_stats_update` but will evict stats cache if needed. Though, stats in temporary files can also get lost. Hence, it is recommended to stick with frequent `select relaccess_stats_update()` calls.
74+
75+
To better understand when it's time to dump or update the stats one might check `select relaccess.relaccess_stats_fillfactor();`. It will show current usage of stats hash table in percents. For example if shared memory for our relaccess hash table is 70% full we will get relaccess_stats_fillfactor=70. It would be a good idea to dump or update when fillfactor is around 70%.
76+
77+
### Limitations and gotchas
78+
There is a number of interesting edge-cases in this simple extension:
79+
* `relaccess_stats_root_tables_aggregated` shows info only about tables that exist **now**. We simply can`t get information about inheritance relationship for deleted tables.
80+
* Stats don't rollback on savepoint rollback. We will see n_select_queries incremented by 2 in the following case:
81+
```sql
82+
BEGIN;
83+
SELECT * FROM tbl;
84+
SAVEPOINT sp;
85+
SELECT * FROM tbl;
86+
ROLLBACK TO SAVEPOINT sp;
87+
COMMIT;
88+
```
89+
There is no technical reason for this limitation. It cat be fixed when there will be need for that.
90+
* Update stats often! Otherwise, data can be lost if any of it happens: 1) there was a crash, 2) `max_tables` exceeded w/o `dump_on_overflow`, 3) temporary pg_stat dir got cleaned.
91+
* no `truncate only` support. There is a TODO in code in case it is ever needed.
92+
* Updates and Deletes also increment n_select_queries. Every update and delete also read the table. That is, n_select_queries get incremented as well. If you need **only** selects, query like this `SELECT n_select_queries - (n_update_queries + n_delete_queries) ... FROM relaccess_stats ...;`. For this same reason last_read and last_reader_id change on update and delete queries.
93+
* view = view + tables. It looks like whenever you select from view, n_select_queries get incremented for both the view and tables it references.
94+
* obviously, we don't know any timestamps before we started tracking. So, the first timestamps are initialized with 0 (something around year 2000), which means those tables haven't been accessed since gp_relaccess_stats was enabled.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# gp_relaccess_stats extension
2+
comment = 'gp_relaccess_stats - facility to track how and when tables, partitions or views were accesseds'
3+
default_version = '1.1'
4+
module_pathname = '$libdir/gp_relaccess_stats'
5+
relocatable = true
6+
trusted = true
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* gp_relaccess_stats--1.0--1.1.sql */
2+
3+
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
4+
\echo Use "ALTER EXTENSION gp_relaccess_stats UPDATE TO '1.1'" to load this file. \quit
5+
6+
DROP VIEW relaccess.relaccess_stats_root_tables_aggregated;
7+
8+
ALTER TABLE relaccess.relaccess_stats ALTER COLUMN n_select_queries TYPE int8;
9+
ALTER TABLE relaccess.relaccess_stats ALTER COLUMN n_insert_queries TYPE int8;
10+
ALTER TABLE relaccess.relaccess_stats ALTER COLUMN n_update_queries TYPE int8;
11+
ALTER TABLE relaccess.relaccess_stats ALTER COLUMN n_delete_queries TYPE int8;
12+
ALTER TABLE relaccess.relaccess_stats ALTER COLUMN n_truncate_queries TYPE int8;
13+
14+
-- This utility view shows **ONLY** stats on **EXISTING** partitioned tables in aggregated form
15+
CREATE VIEW relaccess.relaccess_stats_root_tables_aggregated AS (
16+
WITH RECURSIVE parents AS (
17+
SELECT inhrelid AS child, inhparent AS parent FROM pg_inherits
18+
UNION ALL
19+
SELECT prev.child, next.inhparent AS parent FROM parents AS prev JOIN pg_inherits AS next ON prev.parent = next.inhrelid
20+
), part_to_root_mapping AS (
21+
SELECT DISTINCT child AS partid, min(parent) OVER (partition BY child) AS rootid FROM parents
22+
), parts_including_roots AS (
23+
SELECT rootid as partid, rootid FROM (SELECT DISTINCT rootid FROM part_to_root_mapping) AS p
24+
UNION
25+
SELECT * FROM part_to_root_mapping
26+
), with_root_id AS (
27+
SELECT part_tbl.rootid, stats.* FROM relaccess.relaccess_stats stats JOIN parts_including_roots part_tbl ON (stats.relid = part_tbl.partid)
28+
), without_last_user AS (
29+
SELECT rootid AS relid,
30+
rootid::regclass::text AS relname,
31+
max(last_read) AS last_read,
32+
max(last_write) AS last_write,
33+
sum(n_select_queries) AS n_select_queries,
34+
sum(n_insert_queries) AS n_insert_queries,
35+
sum(n_update_queries) AS n_update_queries,
36+
sum(n_delete_queries) AS n_delete_queries,
37+
sum(n_truncate_queries) AS n_truncate_queries
38+
FROM with_root_id outer_tbl GROUP BY rootid
39+
)
40+
SELECT relid,
41+
relname,
42+
(SELECT last_reader_id FROM with_root_id w WHERE w.rootid = wo.relid AND wo.last_read = w.last_read LIMIT 1) AS last_reader_id,
43+
(SELECT last_writer_id FROM with_root_id w WHERE w.rootid = wo.relid AND wo.last_write = w.last_write LIMIT 1) AS last_writer_id,
44+
last_read,
45+
last_write,
46+
n_select_queries,
47+
n_insert_queries,
48+
n_update_queries,
49+
n_delete_queries,
50+
n_truncate_queries
51+
FROM without_last_user wo
52+
);
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/* gp_relaccess_stats--1.1.sql */
2+
3+
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
4+
\echo Use "CREATE EXTENSION gp_relaccess_stats" to load this file. \quit
5+
6+
CREATE SCHEMA IF NOT EXISTS relaccess;
7+
8+
CREATE TABLE relaccess.relaccess_stats (
9+
relid Oid,
10+
relname Name,
11+
last_reader_id Oid,
12+
last_writer_id Oid,
13+
last_read timestamptz,
14+
last_write timestamptz,
15+
n_select_queries int8,
16+
n_insert_queries int8,
17+
n_update_queries int8,
18+
n_delete_queries int8,
19+
n_truncate_queries int8
20+
) DISTRIBUTED BY (relid);
21+
22+
CREATE FUNCTION relaccess.relaccess_stats_dump()
23+
RETURNS SETOF void
24+
AS 'MODULE_PATHNAME', 'relaccess_stats_dump'
25+
LANGUAGE C VOLATILE EXECUTE ON MASTER;
26+
27+
CREATE FUNCTION relaccess.relaccess_stats_update()
28+
RETURNS SETOF void
29+
AS 'MODULE_PATHNAME', 'relaccess_stats_update'
30+
LANGUAGE C VOLATILE EXECUTE ON MASTER;
31+
32+
CREATE FUNCTION relaccess.relaccess_stats_fillfactor()
33+
RETURNS SETOF INT2
34+
AS 'MODULE_PATHNAME', 'relaccess_stats_fillfactor'
35+
LANGUAGE C VOLATILE EXECUTE ON MASTER;
36+
37+
CREATE FUNCTION relaccess.__get_db_stats_from_dump()
38+
RETURNS SETOF relaccess.relaccess_stats
39+
AS 'MODULE_PATHNAME', 'relaccess_stats_from_dump'
40+
LANGUAGE C VOLATILE EXECUTE ON MASTER;
41+
42+
CREATE FUNCTION relaccess.__relaccess_upsert_from_dump_file() RETURNS VOID
43+
LANGUAGE plpgsql VOLATILE AS
44+
$func$
45+
BEGIN
46+
EXECUTE 'DROP TABLE IF EXISTS relaccess_stats_tmp';
47+
EXECUTE 'CREATE TEMP TABLE relaccess_stats_tmp (LIKE relaccess.relaccess_stats) distributed by (relid)';
48+
EXECUTE 'DROP TABLE IF EXISTS relaccess_stats_tmp_aggregated';
49+
EXECUTE 'CREATE TEMP TABLE relaccess_stats_tmp_aggregated (LIKE relaccess.relaccess_stats) distributed by (relid)';
50+
EXECUTE 'INSERT INTO relaccess_stats_tmp SELECT * FROM relaccess.__get_db_stats_from_dump()';
51+
EXECUTE 'WITH aggregated_wo_relname_and_user AS (
52+
SELECT relid, max(last_read) AS last_read, max(last_write) AS last_write, sum(n_select_queries) AS n_select_queries,
53+
sum(n_insert_queries) AS n_insert_queries, sum(n_update_queries) AS n_update_queries, sum(n_delete_queries) AS n_delete_queries, sum(n_truncate_queries) AS n_truncate_queries
54+
FROM relaccess_stats_tmp GROUP BY relid
55+
)
56+
INSERT INTO relaccess_stats_tmp_aggregated
57+
SELECT relid,
58+
(SELECT relname FROM relaccess_stats_tmp w WHERE w.relid = wo.relid AND greatest(wo.last_read, wo.last_write) IN (w.last_read, w.last_write) LIMIT 1) AS relname,
59+
(SELECT last_reader_id FROM relaccess_stats_tmp w WHERE w.relid = wo.relid AND wo.last_read = w.last_read LIMIT 1) AS last_reader_id,
60+
(SELECT last_writer_id FROM relaccess_stats_tmp w WHERE w.relid = wo.relid AND wo.last_write = w.last_write LIMIT 1) AS last_writer_id,
61+
last_read,
62+
last_write,
63+
n_select_queries,
64+
n_insert_queries,
65+
n_update_queries,
66+
n_delete_queries,
67+
n_truncate_queries FROM aggregated_wo_relname_and_user AS wo';
68+
EXECUTE 'DROP TABLE IF EXISTS relaccess_stats_tmp';
69+
EXECUTE 'INSERT INTO relaccess.relaccess_stats
70+
SELECT relid, relname, last_reader_id, last_writer_id, last_read, last_write, 0, 0, 0, 0, 0
71+
FROM relaccess_stats_tmp_aggregated stage
72+
WHERE NOT EXISTS (
73+
SELECT 1 FROM relaccess.relaccess_stats orig WHERE orig.relid = stage.relid)';
74+
EXECUTE 'UPDATE relaccess.relaccess_stats orig SET
75+
relname = stage.relname,
76+
n_select_queries = orig.n_select_queries + stage.n_select_queries,
77+
n_insert_queries = orig.n_insert_queries + stage.n_insert_queries,
78+
n_update_queries = orig.n_update_queries + stage.n_update_queries,
79+
n_delete_queries = orig.n_delete_queries + stage.n_delete_queries,
80+
n_truncate_queries = orig.n_truncate_queries + stage.n_truncate_queries
81+
FROM relaccess_stats_tmp_aggregated stage
82+
WHERE orig.relid = stage.relid';
83+
EXECUTE 'UPDATE relaccess.relaccess_stats orig SET
84+
last_reader_id = stage.last_reader_id, last_read = stage.last_read
85+
FROM relaccess_stats_tmp_aggregated stage
86+
WHERE orig.relid = stage.relid AND orig.last_read < stage.last_read';
87+
EXECUTE 'UPDATE relaccess.relaccess_stats orig SET
88+
last_writer_id = stage.last_writer_id, last_write = stage.last_write
89+
FROM relaccess_stats_tmp_aggregated stage
90+
WHERE orig.relid = stage.relid AND orig.last_write < stage.last_write';
91+
EXECUTE 'DROP TABLE IF EXISTS relaccess_stats_tmp_aggregated';
92+
END
93+
$func$;
94+
95+
CREATE FUNCTION relaccess.relaccess_stats_init() RETURNS VOID AS
96+
$$
97+
WITH relations AS (
98+
SELECT oid as relid, relname, relowner FROM pg_catalog.pg_class WHERE relkind in ('r', 'v', 'm', 'f', 'p')
99+
)
100+
INSERT INTO relaccess.relaccess_stats
101+
SELECT relid, relname, relowner, relowner, '2000-01-01 03:00:00', '2000-01-01 03:00:00', 0, 0, 0, 0, 0
102+
FROM relations AS all_rels WHERE NOT EXISTS(SELECT 1 FROM relaccess.relaccess_stats orig WHERE orig.relid = all_rels.relid);
103+
$$ LANGUAGE SQL VOLATILE;
104+
105+
-- This utility view shows **ONLY** stats on **EXISTING** partitioned tables in aggregated form
106+
CREATE VIEW relaccess.relaccess_stats_root_tables_aggregated AS (
107+
WITH RECURSIVE parents AS (
108+
SELECT inhrelid AS child, inhparent AS parent FROM pg_inherits
109+
UNION ALL
110+
SELECT prev.child, next.inhparent AS parent FROM parents AS prev JOIN pg_inherits AS next ON prev.parent = next.inhrelid
111+
), part_to_root_mapping AS (
112+
SELECT DISTINCT child AS partid, min(parent) OVER (partition BY child) AS rootid FROM parents
113+
), parts_including_roots AS (
114+
SELECT rootid as partid, rootid FROM (SELECT DISTINCT rootid FROM part_to_root_mapping) AS p
115+
UNION
116+
SELECT * FROM part_to_root_mapping
117+
), with_root_id AS (
118+
SELECT part_tbl.rootid, stats.* FROM relaccess.relaccess_stats stats JOIN parts_including_roots part_tbl ON (stats.relid = part_tbl.partid)
119+
), without_last_user AS (
120+
SELECT rootid AS relid,
121+
rootid::regclass::text AS relname,
122+
max(last_read) AS last_read,
123+
max(last_write) AS last_write,
124+
sum(n_select_queries) AS n_select_queries,
125+
sum(n_insert_queries) AS n_insert_queries,
126+
sum(n_update_queries) AS n_update_queries,
127+
sum(n_delete_queries) AS n_delete_queries,
128+
sum(n_truncate_queries) AS n_truncate_queries
129+
FROM with_root_id outer_tbl GROUP BY rootid
130+
)
131+
SELECT relid,
132+
relname,
133+
(SELECT last_reader_id FROM with_root_id w WHERE w.rootid = wo.relid AND wo.last_read = w.last_read LIMIT 1) AS last_reader_id,
134+
(SELECT last_writer_id FROM with_root_id w WHERE w.rootid = wo.relid AND wo.last_write = w.last_write LIMIT 1) AS last_writer_id,
135+
last_read,
136+
last_write,
137+
n_select_queries,
138+
n_insert_queries,
139+
n_update_queries,
140+
n_delete_queries,
141+
n_truncate_queries
142+
FROM without_last_user wo
143+
);

0 commit comments

Comments
 (0)