6. Database and table operations
By the end of this chapter: You register the tables for GritDB to manage, receive data, view status, and run queries in an analytics session. You know the operations not to perform on registered tables.
- Order of this chapter: register → receive data → view status → analytics sessions → operations not to perform. Loading data (6.8) and managing analytic queries (6.9) follow.
6.1 Create a database
A new database is a copy of template1 (PostgreSQL default, Template databases). GritDB puts the gritsurfer extension in template1, so you can register tables in a new database right away.
CREATE DATABASE <db>;
- To check, connect with
~/gritdb1/bin/psql -p 5432 -d <db>and run\dx gritsurfer. The output has agritsurferline. - Run
SHOW search_path;and check that the value containsgritdb. If it does not, call the registration and status functions with thegritdb.prefix (example:gritdb.register_gritsurfer_table).
Databases created with TEMPLATE template0 or another template do not have the extension. Connect to that database as the service account (superuser) and create the extension.
CREATE EXTENSION gritsurfer;
- To check, run
\dx gritsurfer. The output has agritsurferline. The command is described inCREATE EXTENSION. - Do not drop the
gritsurferextension (DROP EXTENSION). Dropping it also drops the GritDB functions in that database (register, unregister, and status). If you create it again withCREATE EXTENSION gritsurfer, the privileges to run the functions return to the installation default (all accounts). In that case, repeat the revoke steps in 11.5 in that database and check with row 4 of 11.10.
⚠ Do not drop or rename the
postgresdatabase
- Reason: GritDB connects to the
postgresdatabase when it starts. Without it, GritDB does not start.- Instead: Keep business data in a new database (
CREATE DATABASE <db>;).
6.2 Registration requirements
Choose the tables to register.
- Register all tables that you query together. If even one is missing, PostgreSQL runs that query (6.6).
- For a table with very many writes, every write waits for an acknowledgment from the GritDB engine after registration (1.1).
- Do not register tables that have row-level security (RLS) policies or tables that must show different data by privilege (11.5, 11.6). A table that must show different data by privilege is one where each account can read different rows or columns.
The registration requirements are as follows.
REPLICA IDENTITY FULLis required. Without it,register_gritsurfer_tablerejects the table.- If
REPLICA IDENTITY FULLis already set, do not run it again. ThisALTER TABLElocks the table and stops a running autovacuum.- To check, run
SELECT relreplident FROM pg_class WHERE oid = '<s>.<t>'::regclass;. It returnsf.
- To check, run
- Registration targets are ordinary tables and partitioned tables (6.3). Views and foreign tables are rejected first with
Table <s>.<t> must have REPLICA IDENTITY FULL. Current setting: NOTHING. …. These objects cannot have a REPLICA IDENTITY, so do not follow theALTER TABLEat the end of the message. Materialized views and other objects are rejected withGritSurfer: cannot register relation "<t>". - The supported column types are the 15 types below. If even one column has another type, registration is rejected (
GritSurfer: Table '<t>' has unsupported column types - cannot proceed).
| Category | Type |
|---|---|
| Boolean | boolean |
| Integer | smallint, integer, bigint |
| Floating point and numeric | real, double precision, numeric |
| Date and time | date, time, timestamp, timestamptz, interval |
| Character | char(n), varchar, text |
-
Examples of rejected types:
uuid,bytea,json,jsonb, arrays,enum, domains, andtimetz -
The type check (before registration) finds columns with unsupported types. Run it on either the operational database or GritDB.
SELECT a.attname, a.atttypid::regtype FROM pg_attribute a WHERE a.attrelid = '<s>.<t>'::regclass AND a.attnum > 0 AND NOT a.attisdropped AND a.atttypid NOT IN ('boolean'::regtype, 'smallint'::regtype, 'integer'::regtype, 'bigint'::regtype, 'real'::regtype, 'double precision'::regtype, 'numeric'::regtype, 'date'::regtype, 'time'::regtype, 'timestamp'::regtype, 'timestamptz'::regtype, 'interval'::regtype, 'bpchar'::regtype, 'varchar'::regtype, 'text'::regtype);It is normal if the query returns 0 rows.
char(n)isbpchar. -
The quoted 1-byte type
"char"is not supported. It is a different type fromchar(n). -
For
numeric, analytics session values are exact only for columns whose precision is set to 38 or less (numeric(p, s), p ≤ 38). See the precision check below (10.3). -
Register a table while it is empty. Rows that existed before registration are missing from analytics session results (6.3).
-
Without a primary key or a NOT NULL unique constraint, registration still succeeds. However, it shows a WARNING (
… has no PRIMARY KEY or UNIQUE(NOT NULL) constraint. Registering in degraded mode …), and applying updates and deletes is slow. -
In an analytics setup (6.4), always give the table the same primary key as the operational database. Without it, applying updates and deletes stops with an error (6.4.1 step 6).
-
Create tables with a schema name (
CREATE TABLE <s>.<t> …). If you create a table as the service account without a schema, it is created in thegritdbschema (11.5). -
Write schema and table names exactly, with the correct case. Names created without quotes are lowercase (
CREATE TABLE Sales→'sales',CREATE TABLE "Sales"→'Sales'). If a name is wrong, registration is rejected withRelation <s>.<t> not found. -
A table that ever had a column dropped (
DROP COLUMN) is rejected at registration withcache lookup failed for type 0, even if it passes the type check. In that case, create it again with “Rebuild the table” in 6.7 (skip step 4), then register it. To check, runSELECT count(*) FROM pg_attribute WHERE attrelid = '<s>.<t>'::regclass AND attisdropped;. It returns0.
The precision check (before registration) finds numeric columns with no precision or a precision over 38. Run it on either the operational database or GritDB.
SELECT table_schema, table_name, column_name, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE data_type = 'numeric'
AND (numeric_precision IS NULL OR numeric_precision > 38)
AND table_schema NOT IN ('pg_catalog', 'information_schema', 'gritdb')
ORDER BY 1, 2, 3;
- It is normal if the query returns 0 rows. The columns in the result have approximate values in analytics sessions. For amounts and quantities, receive the data into a table created with
numeric(p, s)(p ≤ 38). - To check only the tables you will receive, add
AND table_name IN ('<t1>', '<t2>'). - If the operational database is not PostgreSQL, check whether the CDC or ETL tool creates numeric source columns without precision as
numericwithout precision.
6.3 Register and unregister
| Task | SQL |
|---|---|
| Register | SELECT register_gritsurfer_table('<s>', '<t>'); |
| Unregister | SELECT unregister_gritsurfer_table('<s>', '<t>'); |
- Register a table while it is empty. Rows that existed before registration are not moved to the GritDB engine (10.4).
- ⚠ After you register or unregister a table, reopen the connections that write to it. Each connection checks whether the table is registered when it first writes to it, and keeps that result until the connection ends. So changes from a connection that wrote to the table before registration do not reach the GritDB engine.
- For applications, restart the connection pool.
- For setups that receive data with a CDC or ETL tool, restart the tool’s apply process after you register or unregister.
- In an analytics setup (a setup that receives data through built-in replication), register before you create the subscription. For an existing subscription, run
ALTER SUBSCRIPTION <sub> DISABLE;and confirm that the worker stopped. Then restart the replication worker withALTER SUBSCRIPTION <sub> ENABLE;(6.4.2 step 6).
- Do not register or unregister inside a transaction block (
BEGIN…COMMIT), aDOblock, or a function. AROLLBACKdoes not undo a registration or an unregistration. - Before you unregister, stop writes to the table. You do not need to wait until changes are applied. When you unregister, the GritDB engine discards the rows of that table that are not applied. The unify checkpoint does not stop, so applying changes to the other registered tables does not fall behind. The unify checkpoint is the WAL position up to which the GritDB engine has applied changes durably. It is not the PostgreSQL checkpoint.
⚠ You cannot undo an unregistration
- Reason: Unregistering deletes the table’s cold files (files the GritDB engine stores by column for analytic queries). If you re-register the table, it starts from an empty state.
- Instead: If you only want to pause analytics, do not unregister. To rebuild the table, follow the “Rebuild the table” procedure in 6.7.
- GritDB deletes the cold files of an unregistered table separately, after the unregistration. If you re-register the same table right away, registration waits until that cleanup finishes. While it waits, the
register_gritsurfer_table: waiting for tombstone cleanup …NOTICE appears every 30 seconds.- If the cleanup does not finish within 300 seconds, registration stops with an ERROR. In that case, register a newly created table (6.7 Rebuild the table).
- The table’s cold file directory is created when the first row is applied, not at registration. So it may not exist right after registration.
- To drop a registered table, unregister it first, then run
DROP TABLE. If you runDROP TABLEwithout unregistering, the registration is removed when that transaction commits. To check, confirm that the table is not in the registration list (6.5). AfterROLLBACKorROLLBACK TO SAVEPOINT, the registration stays.DROP DATABASEremoves all registrations in that database. To check, findGritSurfer: dropped database <oid>: unregistered <n> relations (failed 0, …)inpostgresql.log(9.1).- If removing the registration at commit fails, you see a
GritSurfer:WARNING that ends with(registration left in place),(registrations left in place), or(tombstone written, row left). TheDROPcompletes, but the registration stays. In that case, contact support (chapter 0). Do not use the function shown in the HINT. - This WARNING is also written to
postgresql.log(9.1), so look for it there even if the application ignores it. To check, rungrep -c -e 'left in place' -e 'row left' ~/gritdb1/log/postgresql.log. It returns0(7.3). - A transaction that ran
DROPon a registered table or a database cannot runPREPARE TRANSACTION(cannot PREPARE a transaction that dropped a GritDB registered relation or database).
- If removing the registration at commit fails, you see a
- Every table
DROPasks the GritDB catalog whether the table is registered. This also applies to unregistered tables, temporary tables, and tables dropped byDROP SCHEMA … CASCADE. If the catalog does not answer within 5 seconds, thatDROPis rejected withGritSurfer: cannot tell whether relation <oid> is registered (catalog did not answer within 5000 ms). In that case, check the status with 4.5, then try again.
For a partitioned table, register the parent and every partition (including intermediate levels and the DEFAULT partition) one by one. A REPLICA IDENTITY set on the parent does not carry over to the partitions.
-
Find all partitions.
SELECT relid, parentrelid, level, isleaf FROM pg_partition_tree('<s>.<t>') ORDER BY level;- Function:
pg_partition_tree
- Function:
-
Run
ALTER TABLE <table> REPLICA IDENTITY FULL;for each table from step 1. -
Run
register_gritsurfer_tablefor each table in the order from step 1 (parent → partitions, smallestlevelfirst). -
To check, run
SELECT schema_name, table_name, is_partition FROM get_gritsurfer_tables();. The output has all tables from step 1.
- Unregister in reverse order (partitions → parent), then run
DROP TABLE. - After registration, do not attach or detach partitions (6.7).
To register several tables, run one statement per table (a commit per statement). Example that registers tables one after another from a list of names in psql:
SELECT format('SELECT register_gritsurfer_table(%L, %L);', schemaname, tablename)
FROM pg_tables
WHERE schemaname = '<s>' AND tablename IN ('<t1>', '<t2>')
ORDER BY tablename
\gexec
- To check, run
SELECT table_name FROM get_gritsurfer_tables() WHERE schema_name = '<s>';. The output has all the tables you listed. In psql,\gexecruns each cell of the result as a statement. - For partitioned tables, do not use this method. Follow the procedure above.
The register and unregister output is as follows. One registration prints several INFO and NOTICE lines. On success, the output ends with the function name and one empty row (QuickStart 4.4).
| Level | Message (beginning) | Meaning and what to do |
|---|---|---|
| INFO | GritSurfer: Loaded <n> constraints / <n> indexes for table <s>.<t> |
Read the constraints and indexes |
| NOTICE | GritSurfer: Registered relation - …, GritSurfer: Registered toast relation - … |
Registered the table (and its TOAST table) |
| WARNING | GritSurfer: Table '<s>.<t>' has no PRIMARY KEY or UNIQUE(NOT NULL) constraint. … |
Registered without a primary key. Applying updates and deletes is slow (6.2) |
| WARNING → ERROR | GritSurfer: Table '<t>' has unsupported column type: <type oid> → … has unsupported column types - cannot proceed |
Not registered because of an unsupported column type (6.2 type check). To find the type name, run SELECT <type oid>::regtype; |
| NOTICE | register_gritsurfer_table: waiting for tombstone cleanup … |
Waiting for the unregister cleanup (above) |
| ERROR | register_gritsurfer_table: tombstone for OID <oid> not cleared after 300s (GC stalled?) |
The unregister cleanup did not finish within 300 seconds. Register a newly created table (6.7 Rebuild the table) |
| NOTICE | GritSurfer: relation <oid> (<s>.<t>) is already registered |
Already registered. Nothing to do |
| WARNING | GritSurfer: relation <oid> is already registered as <s>.<t> and keeps that name |
A registered table that was renamed. The registration keeps the old name (6.7) |
| ERROR | GritSurfer: relation <oid> (<s>.<t>) is registered with relfilenode <n> but now has <m> |
A table on which VACUUM FULL, CLUSTER, or an ALTER TABLE that rewrites the table was run. Follow “Rebuild the table” in 6.7. Do not use the function shown in the HINT of the error |
| ERROR | GritSurfer: OID <oid> already has a GritDB catalog row for another relation <s>.<t> |
Contact support (chapter 0). Do not use the function shown in the HINT of the error |
| ERROR | Table <s>.<t> must have REPLICA IDENTITY FULL. …, Relation <s>.<t> not found |
A problem with the registration requirements or the name. See QuickStart GDB-RUNTIME-001 |
| NOTICE | GritSurfer: Unregistering relation - … → GritSurfer: table deleted from catalog … |
Unregistered |
| WARNING | GritSurfer: metadata absent, deleting without tombstone … |
You unregistered a table that was not registered. Check the registration list (6.5) |
6.4 Receive into an analytics setup
An analytics setup is a GritDB setup that receives the changes of the operational database and handles analytics (1.1). This section covers the case where the operational database is PostgreSQL and GritDB receives the data through built-in replication (PostgreSQL logical replication, publication → subscription).
- Operational database: PostgreSQL 17. For other major versions and managed services, contact support (chapter 0) before you attach. For details, see 6.4.5.
- The operational database administrator runs the operational database commands on the operational database. The service account runs the GritDB commands on the GritDB server (
~/gritdb1/bin/psql -p 5432 -d <db>). - If the operational database is not PostgreSQL, receive the data with a CDC or ETL tool that writes to PostgreSQL as its target. The GritDB-side steps (6.4.1 steps 6 to 7) are the same. After you register or unregister, restart the tool’s apply process (6.3). Keep the tool’s commit size, including the initial load, below half the single-transaction limit (6.8). For tool settings, follow the tool’s documentation.
- The size check (before you attach) compares two kinds of row counts with the single-transaction limit (6.8). The first is the row count of each table you will receive. The second is the number of rows that operational database batch jobs change in one transaction. If any of them exceeds half the limit, contact support (chapter 0) before you attach.
- Do not use
pg_createsubscriber, which turns a physical standby server into a logical replication subscriber. GritDB does not receive data through physical replication (1.1). For large tables, use the size check above and contact support. - Notation:
<pub>publication,<sub>subscription,<db>database (if the names differ on the operational database side and the GritDB side, use each one),<s>.<t>schema and table,<replication account>,<password>,<operational database address>,<port>(operational database port), and<GritDB server address>
6.4.1 Attach
| Step | Who | What to do |
|---|---|---|
| 1 | Operational database administrator | Check the operational database settings |
| 2 | Operational database administrator | Fix tables without a replica identity |
| 3 | Operational database administrator | Create the publication |
| 4 | Operational database administrator | Create the replication account and allow connections |
| 5 | GritDB administrator | Set the number of workers |
| 6 | GritDB administrator | Create the same tables |
| 7 | GritDB administrator | Register |
| 8 | GritDB administrator | Create the subscription |
| 9 | Both | Check the initial copy and compare counts |
1. Check the operational database settings (operational database)
SHOW wal_level;
SHOW max_replication_slots;
SHOW max_wal_senders;
SHOW max_slot_wal_keep_size;
- It is normal if
wal_levelislogical. If not, runALTER SYSTEM SET wal_level = logical;and restart the operational database once. - Set
max_replication_slotsandmax_wal_sendersto at least the number in use now plus (1 +max_sync_workers_per_subscriptionof GritDB, default 2) for each subscription. If they are too low, change them withALTER SYSTEM SET …, then restart the operational database. Both settings take effect only after a restart, so when you changewal_level, change them at the same time and restart only once. - All settings on the operational database side: Logical replication configuration (publisher)
- The operational database administrator chooses
max_slot_wal_keep_size. With-1(default), WAL accumulates while the analytics setup is stopped, until the operational database disk is full. A limit protects the disk, but if WAL goes over it, you take the tables again from the start (8.6).
2. Find tables without a replica identity (operational database)
SELECT c.oid::regclass AS table_name
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = '<s>'
AND c.relkind IN ('r', 'p')
AND (c.relreplident = 'n'
OR (c.relreplident = 'd'
AND NOT EXISTS (SELECT 1 FROM pg_index i
WHERE i.indrelid = c.oid AND i.indisprimary)));
- It is normal if none of the tables you will receive is in the list.
- For tables in the list, run
ALTER TABLE <s>.<t> REPLICA IDENTITY FULL;before step 3. - With
REPLICA IDENTITY FULL, UPDATE and DELETE on the table write the entire old row to WAL, so WAL grows. Also,ALTER TABLEtakes a lock, so do it at a quiet time. - If you use
REPLICA IDENTITY USING INDEX <index>with a unique index, create the primary key of the GritDB table on the columns of that index (step 6).
⚠ Do not add tables without a replica identity to a publication
- Reason: UPDATE and DELETE on that table fail with an error on the operational database (
cannot update table "<t>" because it does not have a replica identity and publishes updates).- Instead: Set the replica identity first, in step 2.
3. Create the publication (operational database)
CREATE PUBLICATION <pub> FOR TABLE <s>.<t1>, <s>.<t2>;
SELECT schemaname, tablename FROM pg_publication_tables WHERE pubname = '<pub>';
- It is normal if only the tables you added appear. The command and view are described in
CREATE PUBLICATIONandpg_publication_tables. - Add only the tables to analyze, by name. Do not use
FOR ALL TABLESorFOR TABLES IN SCHEMA. With them, tables created later would come in without the step 2 check and without preparation on GritDB. - Leave the
publishoption at its default of insert, update, delete, and truncate (6.4.2). - Do not add tables that operational database batch jobs empty with
TRUNCATEand fill again. If you add them, you must take that table again (6.4.2, including the initial copy) after everyTRUNCATE. If you do not, deleted rows remain in analytics sessions. So before you attach, ask the operational database administrator which batch jobs useTRUNCATE. - Put only one table with many rows in the first publication. Add the other tables one at a time after step 9 shows
r(6.4.2 “Add a table to an existing analytics setup”, 6.8). - If the operational database has a
gritdbschema, do not add the tables in that schema. GritDB rejects them.
4. Create the replication account and allow connections (operational database)
CREATE ROLE <replication account> WITH LOGIN REPLICATION;
\password <replication account>
GRANT USAGE ON SCHEMA <s> TO <replication account>;
GRANT SELECT ON <s>.<t1>, <s>.<t2> TO <replication account>;
- With
\password, psql encrypts the password before it sends it, so the password does not remain in the operational database log or in~/.psql_history(11.3). With a tool other than psql, use that tool’s password change dialog. - If
CONNECTwas revoked fromPUBLICon the operational database, also runGRANT CONNECT ON DATABASE <db> TO <replication account>;.
Add one line to pg_hba.conf, then run SELECT pg_reload_conf();.
host <db> <replication account> <GritDB server address>/32 scram-sha-256
-
In the firewall, allow only connections from the GritDB server to the operational database port.
-
To check, run
~/gritdb1/bin/psql "host=<operational database address> port=<port> dbname=<db> user=<replication account> replication=database" -c "IDENTIFY_SYSTEM;"on the GritDB server. After you enter the password, it returns one line withsystemid,timeline,xlogpos, anddbname. This checks the login,pg_hba.conf, and the replication privilege together (Replication connections). -
GritDB 1.1 has no connection encryption (TLS), so attach only within the same private network (11.8). If the operational database accepts only encrypted connections through
hostsslor the SSL enforcement setting of a managed service, you cannot attach. -
Check whether operational database tables have row-level security (RLS) policies. If the replication account does not have
BYPASSRLS, the initial copy reads only the rows that the policies allow, and rows go missing in GritDB without an error.SELECT c.oid::regclass FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_publication_tables p ON p.schemaname = n.nspname AND p.tablename = c.relname WHERE p.pubname = '<pub>' AND c.relrowsecurity;It is normal if the query returns 0 rows. If every analytics account on GritDB may read all rows of a table in the result (11.5 security requirements for registered tables), give the replication account
BYPASSRLS. If not, remove the table from the publication, because analytics sessions do not apply RLS (11.6). The connection string of step 8 hasoptions=-crow_security=off. With it, the copy stops with an error when it meets such a table, instead of leaving rows out (Logical replication security).
5. Set the number of workers (GritDB)
SHOW max_worker_processes;
SHOW max_logical_replication_workers;
SHOW max_sync_workers_per_subscription;
SHOW max_parallel_workers;
SHOW max_replication_slots;
- Required values
max_logical_replication_workers≥ number of subscriptions × (1 +max_sync_workers_per_subscription)max_worker_processes≥ 6 (GritDB workers: 5 that always run + 1 during start) + 1 (logical replication launcher) +max_logical_replication_workers+max_parallel_workersmax_replication_slots(GritDB) ≥ number of subscriptions × (1 +max_sync_workers_per_subscription). Progress records on the subscriber side also count against this value.
- Installation defaults (PostgreSQL defaults):
max_worker_processes8,max_logical_replication_workers4,max_sync_workers_per_subscription2,max_parallel_workers8,max_replication_slots10. With these defaults,max_worker_processesmust be 19 or more even with one subscription. So raise it when you attach for the first time (example:ALTER SYSTEM SET max_worker_processes = 24;). Keep the value at 256 or less (⚠ below). - If they are too low, run
ALTER SYSTEM SET <setting> = <value>;and follow the restart procedure in 5.4. After the restart, check for theshm_limit_mbwarning in 5.6 (5.4). - Before step 8, reduce the number of tables copied at the same time to one (6.8). This keeps two initial copies from sharing the single-transaction limit. Run
ALTER SYSTEM SET max_sync_workers_per_subscription = 1;and thenSELECT pg_reload_conf();(no restart). When step 9 showsrfor all tables, runALTER SYSTEM RESET max_sync_workers_per_subscription;and thenSELECT pg_reload_conf();. - Settings:
max_logical_replication_workers,max_sync_workers_per_subscription. All subscriber-side settings: Logical replication configuration (subscriber) - ⚠ If there are not enough worker slots, GritDB may fail to start, so set them before you create or add subscriptions.
- ⚠ Do not set
max_worker_processeshigher than 256 (GritDB does not start).
6. Create the same tables (GritDB)
- First create the database that receives the data (6.1). Do not use the
postgresdatabase. Do all GritDB-side work in steps 6 to 9 while connected to that database (~/gritdb1/bin/psql -p 5432 -d <db>). A subscription applies only to tables in the database where it is created (CREATE SUBSCRIPTION). - Use the same schema, table name, column names, column types, and primary key as the operational database. The column order can differ.
- Example: Extract the definition on the operational database with
pg_dump --schema-only --no-owner --no-privileges --table=<s>.<t> <db>and run it on GritDB. If the schema does not exist, first runCREATE SCHEMA <s>;. The two options leave out the owner and privilege lines of the operational database. Adjust the trigger lines for your environment. - Delete the
ENABLE ROW LEVEL SECURITYandCREATE POLICYlines from thepg_dumpoutput. Do not register tables with RLS policies (11.6). To check, run the check query in 11.6 after step 7. It returns 0 rows. - Use only the 15 column types (6.2).
- Create
numericcolumns asnumeric(p, s)with p ≤ 38 (6.2 precision check). If the operational database column isnumericwithout precision, first measure the largest number of digits in the operational database values and set a precision with room to spare. If a larger value arrives, apply stops (numeric field overflow), and digits beyond the scale are rounded. - Registration requirement:
ALTER TABLE <s>.<t> REPLICA IDENTITY FULL; - To check, run
\d+ <s>.<t>. The output has a primary key andReplica Identity: FULL.\ddoes not show this line.
⚠ Do not create tables without a primary key
- Reason: If the operational database replicates by primary key or unique index, applying updates and deletes on GritDB stops with an error (
logical replication target relation "<s>.<t>" has neither REPLICA IDENTITY index nor PRIMARY KEY and published relation does not have REPLICA IDENTITY FULL). While apply is stopped, WAL accumulates on the operational database.- Instead: Give the table the same primary key as the operational database (if the operational database uses
REPLICA IDENTITY USING INDEX, on the columns of that index). If the operational database table isREPLICA IDENTITY FULL, apply does not stop, but applying updates and deletes is slow.
7. Register (GritDB, before the subscription)
SELECT register_gritsurfer_table('<s>', '<t>');
SELECT * FROM get_gritsurfer_tables();
- It is normal if all the tables you will receive are in the list. The registration output is in 6.3.
- If you register after you create the subscription, changes to the table may not reach the analytics data (6.3, 10.4).
8. Create the subscription (GritDB)
CREATE SUBSCRIPTION <sub>
CONNECTION 'host=<operational database address> port=<port> dbname=<db> user=<replication account> password=<password> options=-crow_security=off'
PUBLICATION <pub>
WITH (streaming = off);
Expected output:
NOTICE: created replication slot "<sub>" on publisher
CREATE SUBSCRIPTION
SELECT oid, subname, subenabled, substream FROM pg_subscription;
- It is normal if
subenabledistandsubstreamisf. - Write down the
oidvalue. When you clean up, it identifies the slots on the operational database (6.4.4). The command is described inCREATE SUBSCRIPTION. options=-crow_security=offmakes sure that you do not receive data with rows left out by row-level security policies on the operational database (step 4).streaming = offis the default, but write it anyway. Do not useonorparallel.- To keep the password out of the connection string, put one line
<operational database address>:<port>:<db>:<replication account>:<password>in the service account’s~/.pgpass(permissions600). Then removepassword=<password>from the connection string. A subscription that the service account creates connects with the password in this file, so the password does not remain inpg_subscription, the logs, or~/.psql_history. When the password changes, edit only this file (.pgpass). - If you put the password in the connection string, it remains in
pg_subscriptionon GritDB. Only superusers can read it (11.5). - If
log_statementisddlor higher, the password in this statement remains inpostgresql.log. If the statement fails (example:could not connect to the publisher), the whole statement remains as aSTATEMENT:line regardless of thelog_statementvalue. This is because the default oflog_min_error_statementiserror. So in the session that creates the subscription, first runSET log_statement = 'none';andSET log_min_error_statement = 'panic';. Also check~/.psql_history(11.7, 11.9). - If a statement with the password is already in the logs, change the replication account password on the operational database (
\password <replication account>). If the subscription already exists, edit the line in the service account’s~/.pgpassor runALTER SUBSCRIPTION <sub> CONNECTION '<new connection info>';to give it the new password (6.4.5). Run this statement after the twoSETstatements above, too. Then check with step 9. Before you send logs outside, delete the log lines that contain the password (9.7, QuickStart 7.4). - The initial copy of each table arrives as one transaction (warning below, 6.8).
⚠ Do not use a subscription to receive a table with more rows than half the single-transaction limit
- Reason: The initial copy loads one table as one transaction. If it exceeds the limit (6.8), GritDB stops; when you start GritDB again, it repeats the same copy from the beginning and stops again.
- Instead: Before step 3, measure with
SELECT count(*) FROM <s>.<t>;on the operational database. If the count exceeds half the limit, contact support before you attach (chapter 0).
9. Check the initial copy (GritDB → operational database)
SELECT srrelid::regclass AS table_name, srsubstate FROM pg_subscription_rel;
- It is normal if
srsubstateisrfor all tables.i,d,f, andsmean that the copy is in progress (pg_subscription_rel). - Compare counts in the order below.
- Stop writes to the table on the operational database, or choose a time without writes. Then write down the value of
SELECT pg_current_wal_lsn();. - On the operational database, run
SELECT confirmed_flush_lsn >= '<value from step 1>'::pg_lsn FROM pg_replication_slots WHERE slot_name = '<sub>';. It returnst(pg_replication_slots). - Confirm that changes are applied on GritDB (6.6.1).
- Measure on the operational database, in a GritDB regular session, and in a GritDB analytics session.
- Stop writes to the table on the operational database, or choose a time without writes. Then write down the value of
Operational database and GritDB regular session:
SELECT count(*), sum(<numeric column>) FROM <s>.<t>;
GritDB analytics session (6.6):
/*+ engine=burst, fallback=false */ SELECT count(*), sum(<numeric column>) FROM <s>.<t>;
- It is normal if the three values are the same.
- For
<numeric column>, choose an integer column or anumeric(p, s)column. The sum of arealcolumn andavgcan differ between the two sessions (6.6).
6.4.2 When you run TRUNCATE or change columns on the operational database
| Operation on the operational database | In GritDB 1.1 | What to do |
|---|---|---|
TRUNCATE |
The table is empty in regular sessions. Deleted rows remain in analytics sessions | Take that table again |
| Add a column or rename a column | Apply stops (is missing replicated column). Meanwhile, WAL accumulates on the operational database |
Take that table again (before the operation) |
| Drop a column | If the column on GritDB is NOT NULL and has no default, apply stops when a new row arrives (null value in column "<column>" of relation "<t>" violates not-null constraint). Meanwhile, WAL accumulates on the operational database. Otherwise, apply does not stop, and the column has an empty value in new rows (its default, if it has one) |
Take that table again (before the operation). To find whether apply stops, run \d <s>.<t> on GritDB. Apply stops if that column shows not null under Nullable and an empty Default |
| Change a column type | Apply can stop, or values can be wrong | Take that table again (before the operation) |
| Rename the table | Apply can stop (does not exist) |
Take that table again (before the operation) |
Use a sequence (nextval) |
Sequence values are not replicated (table rows are) | Nothing to do (no impact on analytics) |
During “Take that table again”, the other tables keep receiving data. That table runs the initial copy again. So if its row count exceeds half the single-transaction limit (6.8), contact support before you start (warning in 6.4.1 step 8). If you receive data with a CDC or ETL tool, see the end of this section. Before step 1, get the new definition (columns to add and types to change) from the operational database administrator. Check it against the 15 types in 6.2 and numeric(p, s) (p ≤ 38). If a column with an unsupported type appears, registration in step 5 is rejected. In that case, contact support (chapter 0) before you do step 1.
| Step | Where | What to do | Normal |
|---|---|---|---|
| 1 | Operational database | Run ALTER PUBLICATION <pub> DROP TABLE <s>.<t>;. In the same session, write down the value of SELECT now(); (time of DROP). Wait until the transactions that were already open at that time end. Write down the value of SELECT pg_current_wal_lsn(); |
Wait until SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend' AND xact_start < '<time of DROP>'; returns 0 |
| 2 | GritDB | ALTER SUBSCRIPTION <sub> REFRESH PUBLICATION; |
The table is not in pg_subscription_rel |
| 3 | Operational database | SELECT confirmed_flush_lsn >= '<value from step 1>'::pg_lsn FROM pg_replication_slots WHERE slot_name = '<sub>'; |
Wait until it is t |
| 4 | Operational database | The operation (TRUNCATE, column change, or rename) |
The command finishes |
| 5 | GritDB | Record and drop the privileges and dependent objects (views, materialized views, and foreign keys). Then follow this order: unregister → DROP TABLE → CREATE TABLE with the new definition (same primary key) → REPLICA IDENTITY FULL → register → re-create privileges and views. Get the new definition with pg_dump --schema-only --no-owner --no-privileges --table=<s>.<t> <db> on the operational database after the operation in step 4 (6.4.1 step 6). Re-create materialized views and foreign keys after step 9 |
The table is in the registration list |
| 6 | GritDB | Run ALTER SUBSCRIPTION <sub> DISABLE; and confirm that the worker stopped. Then run ALTER SUBSCRIPTION <sub> ENABLE; |
The worker has stopped if SELECT pid FROM pg_stat_subscription WHERE subname = '<sub>'; returns an empty value |
| 7 | Operational database | ALTER PUBLICATION <pub> ADD TABLE <s>.<t>; |
The table is in pg_publication_tables |
| 8 | GritDB | ALTER SUBSCRIPTION <sub> REFRESH PUBLICATION; |
Only that table runs the initial copy (srsubstate in 6.4.1 step 9) |
| 9 | Both | 6.4.1 step 9 (that table) | The three values are the same |
- PostgreSQL rules for what is replicated and what is not (DDL, sequences, and TRUNCATE): Logical replication restrictions. Commands:
ALTER PUBLICATION,ALTER SUBSCRIPTION. Workers:pg_stat_subscription - The wait in step 1 makes sure that step 3 also receives changes from open transactions. These transactions wrote to the table before
DROP TABLEand have not committed yet. If the operational database uses two-phase commit (SHOW max_prepared_transactions;is greater than 0), after the value above becomes0, also wait untilSELECT count(*) FROM pg_prepared_xacts;is0. This is because prepared transactions are not inpg_stat_activity(pg_prepared_xacts). - Step 3 checks that GritDB has received all changes written before step 1. If you skip it and do step 5, those changes arrive for the dropped table and the whole subscription stops (
logical replication target relation "<s>.<t>" does not exist). - In step 5, register a newly created table. If you empty the same table and re-register it right away, registration can wait for the unregister cleanup and fail with an ERROR after 300 seconds.
- In step 5, check the privileges with
\dp <s>.<t>on GritDB. Find the dependent objects with the query in step 5 of “Rebuild the table” in 6.7 (pg_depend). Record views and materialized views withpg_dump --schema-only --table=<view>, then drop them. For foreign keys in other tables, record the definitions and drop them (same step 5). If dependent objects exist,DROP TABLEis rejected (cannot drop table … because other objects depend on it). Do not useCASCADE. Re-create views at the end of step 5. Re-create materialized views and foreign keys after the three values match in step 9. For materialized views, also runREFRESH MATERIALIZED VIEW <view>;. If you setALTER DEFAULT PRIVILEGESon the schema (11.5), the analytics account getsSELECTautomatically. - Step 6 makes the replication worker read the new registration (6.3). If the worker is busy, it may not go down between
DISABLEand anENABLEright after it. So confirm that it stopped (pidis empty), then enable the subscription. - If the operation already ran on the operational database and the subscription did not stop (
TRUNCATE, or dropping a column that is notNOT NULL), do all steps except step 4. If apply already stopped because of adding a column, changing a type, renaming, or dropping aNOT NULLcolumn (is missing replicated column,does not exist, orviolates not-null constraint), this procedure alone does not fix it. In that case, contact support (chapter 0).
In “Add a table to an existing analytics setup”, first check the row count, the replica identity (step 2 in 6.4.1), and row-level security on the operational database. If the row count exceeds half the limit, contact support (warning in 6.4.1 step 8). To check row-level security, run SELECT relrowsecurity FROM pg_class WHERE oid = '<s>.<t>'::regclass;. It returns f (step 4). Give the replication account GRANT SELECT (step 4). For another schema, also give it GRANT USAGE ON SCHEMA <s>. Then create and register the table on GritDB (steps 6 and 7). Next, do steps 6 to 9 of the table above. Add one table at a time (6.8).
When you receive data with a CDC or ETL tool:
- Remove the table from the tool (tool documentation).
- Unregister the table, then run
DROP TABLE. - Run
CREATE TABLEwith the new definition (same primary key andREPLICA IDENTITY FULL), then register the table. - Restart the tool’s apply process.
- Load the table from the beginning with the tool. Keep the commit size below half the single-transaction limit (6.8). Then compare as in 6.4.1 step 9.
⚠ Do not stop the subscription (
DISABLE) and run TRUNCATE on the operational database in the meantime
- Reason: Changes to the table made while the subscription was stopped, and changes not yet received before it stopped, arrive after you enable it again. Deleted rows remain in analytics sessions.
- Instead: Follow “Take that table again” above.
6.4.3 What happens on the operational database
- Normally, the WAL sender process (walsender) on the operational database decodes the changes and sends them to GritDB.
- Because of
streaming = off, the operational database collects the changes of a large transaction until the transaction ends, then sends them. When the changes exceed the memory limitlogical_decoding_work_mem, it collects them on disk. Meanwhile, this uses disk space on the operational database (pg_replslot). GritDB also applies them as one transaction.- Also split operational database batch jobs and bulk UPDATE and DELETE, and commit them below half the GritDB single-transaction limit (6.8). Agree on this with the operational database administrator.
- If a transaction exceeds the limit, GritDB stops. When you start GritDB again, it receives the same transaction again and stops again. In that case, contact support (chapter 0).
- During the initial copy, a long read transaction runs on the operational database. Meanwhile, VACUUM on the operational database cannot remove dead rows created after it started. So attach large tables at a quiet time.
- For tables whose replica identity is set to
REPLICA IDENTITY FULL, every UPDATE and DELETE writes the entire old row to WAL (6.4.1 step 2). - If the subscription stops (GritDB is stopped, or an apply error occurs), the operational database slot holds WAL, and the operational database disk fills up.
- The limit is
max_slot_wal_keep_sizeon the operational database (6.4.1 step 1). If WAL exceeds it, the operational database slot becomes invalid. In that case, take the analytics setup again from the beginning (8.6). - ⚠ Do not set
max_slot_wal_keep_sizeon the GritDB server (10.5).
- The limit is
- For monitoring, see 7.5.
6.4.4 Clean up (when you end a subscription, including at the end of a PoC)
| Step | Where | What to do | Normal |
|---|---|---|---|
| 1 | GritDB | Write down the value of SELECT oid FROM pg_subscription WHERE subname = '<sub>';, then run DROP SUBSCRIPTION <sub>; |
NOTICE: dropped replication slot "<sub>" on publisher |
| 2 | Operational database | SELECT slot_name FROM pg_replication_slots WHERE slot_name = '<sub>' OR slot_name LIKE 'pg\_<oid from step 1>\_sync\_%'; |
0 rows |
| 3 | Operational database | Drop only the slots from step 2 with SELECT pg_drop_replication_slot('<slot>'); |
Running step 2 again returns 0 rows |
| 4 | Operational database | Run DROP PUBLICATION <pub>;, and remove the replication account and the pg_hba.conf line |
— |
| 5 | GritDB | Unregister the tables, then run DROP TABLE (6.3) |
Not in the registration list |
- If step 1 cannot reach the operational database, follow this order:
ALTER SUBSCRIPTION <sub> DISABLE;→ALTER SUBSCRIPTION <sub> SET (slot_name = NONE);→DROP SUBSCRIPTION <sub>;→ steps 2 and 3 (Replication slot management). - Command and function:
DROP SUBSCRIPTION,pg_drop_replication_slot - If GritDB is lost and you cannot do step 1, do step 2 with the
oidthat you wrote down in 6.4.1 step 8. - You can leave
wal_levelas it is. Changing it back takes one operational database restart.
⚠ Do not leave slots on the operational database
- Reason: If you remove GritDB first, or finish without dropping the subscription, the operational database slot keeps holding WAL and fills the disk.
- Instead: Clean up in the order 1 → 2, and confirm with step 2.
⚠ Do not drop slots that step 2 does not show
- Reason: If the slot belongs to a standby server, another subscription, or a CDC tool, that replication breaks and cannot be restored.
- Instead: Drop only the slots that match the condition in step 2.
6.4.5 When the operational database changes · managed services
| What changes on the operational database | What happens to the analytics setup | What to do |
|---|---|---|
| Restart | The subscription resumes receiving | Only check (below) |
| Failover or switchover | The subscription slot is on the old primary, so the analytics setup stops | Take the tables again from the start, from the new primary (8.6) |
| Major upgrade | Handling of subscriptions and slots is not tested in 1.1 | Contact support (chapter 0) before you upgrade |
| Address | The subscription tries to connect to the old address and fails | Change the connection string (below) |
- PostgreSQL 17 failover slots (
failover = true) are not tested in 1.1. To use them, first contact support (chapter 0). - To check after a restart or an address change, run
SELECT pid FROM pg_stat_subscription WHERE subname = '<sub>';on GritDB. It returns a value. Then compare counts as in 6.4.1 step 9.
If the operational database address changes, change the connection string on GritDB.
ALTER SUBSCRIPTION <sub>
CONNECTION 'host=<new operational database address> port=<port> dbname=<db> user=<replication account> password=<password> options=-crow_security=off';
- This statement also contains the password, so keep it out of the logs and
~/.psql_historyas in 6.4.1 step 8. If you use.pgpass, the connection string has no password. In that case, also fix the address in that file’s line (6.4.1 step 8). - If the GritDB server address changes, update the
pg_hba.confline and the firewall of the operational database to the new address (6.4.1 step 4). For allowing connections on the GritDB side, see 11.4.
For managed services (PostgreSQL from a cloud provider), contact support (chapter 0) before you attach. Differences from standard PostgreSQL:
- Turn on logical replication with the service’s parameter settings instead of
wal_level(restart required). - Grant the replication account the replication role that the service defines.
- If SSL is enforced by default, GritDB 1.1 cannot attach (6.4.1 step 4).
6.5 View status
| Function | What it shows |
|---|---|
gritsurfer_version() |
Extension SQL version, not the GritDB version number. For the version number, see chapter 0 |
get_gritsurfer_tables(), show_gritsurfer_tables() |
Registered table list and summary (including rows for TOAST tables) |
get_gritsurfer_columns(), get_gritsurfer_constraints(), get_gritsurfer_indexes() |
Columns, constraints, and indexes of registered tables |
get_gritsurfer_coldfiles(), show_gritsurfer_coldfiles() |
Status of cold files |
get_gritsurfer_unify_checkpoints(), show_gritsurfer_unify_checkpoints() |
Status of unify checkpoints |
get_gritsurfer_active_queries(), show_gritsurfer_active_queries() |
Analytic queries in progress |
get_gritsurfer_query_history(max_rows integer DEFAULT NULL, since timestamptz DEFAULT NULL), show_gritsurfer_query_history(same arguments) |
Analytic query history. With no arguments, it shows everything. max_rows shows only that many of the most recent rows (NULL means no limit). since shows only rows after that time (NULL means the whole period). If the history is large, use the two arguments to set a window |
get_*functions return a table for processing with SQL.show_*functions return text for people to read.- The status functions show only the objects of the database you are connected to. Only
get_gritsurfer_unify_checkpoints()is common to all databases. - If there are several lines for registrations with the same name, an old registration was not cleaned up and remains (the
left in placeorrow leftWARNING in 6.3). The line with the largestregister_tsis the current registration. In that case, contact support (chapter 0). gritdb.grit_surfer_heartbeatin thepostgresdatabase is an internal GritDB registered table (written every 10 seconds). Do not unregister or drop it (10.5).- The analytic query history is recorded shortly after a query ends (usually within 1 second).
- The arguments of the history functions can differ between installation package versions, so check them with
\df get_gritsurfer_query_historybefore you use them (3.5). get_gritsurfer_constraints()andget_gritsurfer_indexes()show the definitions read at registration. Constraints and indexes changed after registration do not appear. For the current definitions, run\d <s>.<t>.
Status of cold files (status in get_gritsurfer_coldfiles()):
| Status | Meaning |
|---|---|
STAGED |
Created but not yet used by queries (before the unify checkpoint) |
ACTIVE |
Used by queries, and a target of the next compaction. Compaction is the GritDB engine’s work of merging small cold files into larger cold files |
FROZEN |
Used by queries, and does not change anymore |
INACTIVE |
Being deleted |
View the number and size of files by table and by status.
SELECT schema_name, table_name, status, count(*) AS files,
pg_size_pretty(sum(file_size_bytes)) AS size
FROM get_gritsurfer_coldfiles()
GROUP BY schema_name, table_name, status
ORDER BY schema_name, table_name, status;
- If
STAGEDkeeps growing, the unify checkpoint has stopped. In that case, see chapter 7. The thresholds are in 7.2.
To see whether the unify checkpoint (explained in 6.3) has stopped, look at the time of the newest row.
SELECT max(created_at) AS last_checkpoint,
now() - max(created_at) AS age
FROM get_gritsurfer_unify_checkpoints();
- It is normal if
agedoes not keep growing when you run the query several times. The checkpoint advances even without writes (6.6.1). - If
agekeeps growing, see chapter 7.
6.6 Analytics sessions
Set the session mode (gritsurfer_session_mode) when you connect. You cannot change it after you connect (SET is rejected).
| Value | Behavior |
|---|---|
pg (default) |
PostgreSQL runs all queries. Hint comments (/*+ ... */) are ordinary comments |
surfer |
Among the queries that read registered tables, the GritDB engine runs the queries that meet the engine conditions. PostgreSQL runs all other queries |
Connect as an analytics session.
PGOPTIONS='-c gritsurfer_session_mode=surfer' ~/gritdb1/bin/psql -p 5432 -d <db>
Expected output (first three lines):
NOTICE: GritSurfer session started: default_transaction_read_only is on
DETAIL: Cold-file synchronization is asynchronous, so a query inside a transaction that mixes writes and reads is not guaranteed to see the written data.
HINT: Read-only usage is recommended. For write workloads, connect with gritsurfer_session_mode=pg.
For applications, put -c gritsurfer_session_mode=surfer in the connection options. Only the place to put it differs by driver.
| Driver or tool | Where to put it | Value |
|---|---|---|
| libpq, psql, psycopg | options in the connection string (for psql, also PGOPTIONS) |
options='-c gritsurfer_session_mode=surfer' |
libpq URI (postgresql://…) |
Query parameter | options=-c%20gritsurfer_session_mode%3Dsurfer |
| JDBC (PostgreSQL JDBC driver) | Connection property options (in a URL, a space is %20) |
options=-c%20gritsurfer_session_mode=surfer |
| .NET (Npgsql 5.0 or later) | Options in the connection string |
Options=-c gritsurfer_session_mode=surfer |
| BI and SQL tools | Additional connection properties in the connection settings, JDBC URL, or driver properties | The value for the driver that the tool uses |
-
To check, run
SHOW gritsurfer_session_mode;in that connection. It returnssurfer. -
You cannot turn it on with SQL that runs after the connection is made (such as the initial SQL of a BI tool).
SETis rejected. Tools that have no field for it connect as a regular session. -
Analytics sessions are turned on with a connection option. GritDB 1.1 has no setting that limits which accounts can turn them on. So for the security requirements for registered tables, see 11.5.
-
⚠ You cannot turn it on with account or database defaults (
ALTER ROLE,ALTER DATABASE … SET). Those statements are rejected (parameter "gritsurfer_session_mode" cannot be set after connection start). -
⚠ Do not set it globally with
postgresql.conforALTER SYSTEM. Even the GritDB internal work connections become analytics sessions. -
Use analytics sessions as read-only.
- If
gritsurfer_write_visibility_guardison(default), the session starts withdefault_transaction_read_only= onand shows an informational NOTICE. In this state, a write fails with an ERROR (cannot execute INSERT in a read-only transaction). If the session turnsdefault_transaction_read_onlyoff, the first write shows a WARNING. - Reason: Data written in an analytics session is not applied to cold files right away. Queries in the analytics session may not see that data.
- Do writes (INSERT, UPDATE, DELETE, and DDL) in a regular session (
pg). - Only a superuser can change this setting.
- If
Statement hints are read only in analytics sessions. Only the first /*+ ... */ comment in a statement is read. Keys and values are not case-sensitive. A hint with a wrong format stops the statement with an ERROR.
| Hint | Meaning |
|---|---|
/*+ engine=pg */ |
PostgreSQL runs this statement |
/*+ engine=burst */ |
Sends this statement to the GritDB engine. If the statement is not a SELECT or reads a table that is not registered, it stops with an ERROR. If it has unsupported functions or types, it shows a WARNING and sends the statement anyway |
/*+ fallback=false */ |
If engine execution fails, raises an error instead of re-running the statement on PostgreSQL |
- To use both keys together, write
/*+ engine=burst, fallback=false */. - The default of
fallbackisgritsurfer_session_fallback. When this value ison, a statement is re-run on PostgreSQL if engine execution fails.
The GritDB engine runs statements that meet all of the conditions below. If even one is not met, PostgreSQL runs the statement without a message.
| Condition | Details |
|---|---|
| Statement | A SELECT, not a cursor |
| Tables read | All are registered tables (including tables in subqueries, CTEs, and views). The statement does not read materialized views. The FROM clause has no functions (example: generate_series) |
| Functions and operators | Uses only built-in PostgreSQL functions and operators. No user-defined functions |
| Result column types | The 15 types (6.2) |
In an analytics session, check which engine runs a statement with EXPLAIN.
EXPLAIN SELECT count(*) FROM <s>.<t>;
- If the first line is
engine: GritBurst, the GritDB engine runs the statement (forEXPLAIN ANALYZE,engine: GritBurst (analyze)). Without that line, PostgreSQL runs it. - The analytic query history (6.5) records only statements sent to the GritDB engine. A statement that was sent and then re-run on PostgreSQL is recorded as
FAILED.
Re-run (fallback):
- If
fallbackis on, only statements that fail before the GritDB engine sends results are re-run on PostgreSQL. In that case, you see the WARNINGGritSurfer: falling back to standard PostgreSQL plan: <reason>. - A failure after the engine starts to send results is not re-run. The statement stops with an ERROR (SQLSTATE 38000).
PREPAREand the extended protocol (bind execution in drivers) run on the engine chosen at prepare time and are not re-run. Changing the hint at execution time does not change the engine (WARNING… (prepare-time routing is sticky)).
Concurrency:
- The GritDB engine runs up to vCPUs × 4 analytic queries at the same time. Beyond that, queries wait until an execution slot is free.
- If a query waits longer than
grit_burst.session.connect_timeout_ms(default 300 seconds, 5.6), it runs on PostgreSQL. - However, bind execution in drivers (such as BI tools that use JDBC),
PREPARE, andfallback=falsedo not run on PostgreSQL when the time limit passes. They stop with an ERROR (GritSurfer: failed to send query to burst engine, SQLSTATE 38000). - While a query waits for an execution slot, cancel (Ctrl-C) and
statement_timeoutdo not work until the wait ends. To stop it right away, usepg_terminate_backend(6.9). - To check the limit, see the
cap=value in the line below.
grep -h 'channel capacity: service Session' ~/gritdb1/log/grit_admin_main.log* | tail -1
Apply lag:
- Analytics sessions see committed changes after a short delay. Changes usually become visible in about 12 seconds, and 99 times out of 100 within about 22 seconds (in-house tests).
- In an analytics setup, the replication time from the operational database is added.
- Run queries that must have the latest values in a regular session (in an analytics setup, on the operational database).
- To check whether a change you just committed is visible, see 6.6.1.
Cases where results can differ from PostgreSQL:
- Precision of
avg, digits ofnumeric,sumofreal(float4) columns,octet_lengthofchar(n), andconcat - When the GritDB engine shows
timestamptzvalues, it does not use the session time zone (TimeZone). So convert them withAT TIME ZONE'<time zone>'in the query (1.8). To check, runSHOW timezone;in analytics connections, including BI tools. It returns the same value as the server time zone (5.3). If it differs, adjust the time zone setting of the tool. For JDBC tools, set the JVM time zone (example:-Duser.timezone=Asia/Seoul). If the time zones do not match, date and time values differ between statements that the GritDB engine runs and statements that PostgreSQL runs, even within one session. - If you need exact values, query in a regular session or add
/*+ engine=pg */.
Connection pooler:
- If you need a connection pooler, use Pgpool-II. Pgpool-II passes the connection options (
options) to GritDB as they are, so analytics sessions open even through the pooler. - PgBouncer does not pass connection options. Connections through PgBouncer cannot open analytics sessions. So connect analytics connections directly to GritDB, not through PgBouncer.
- Through a pooler, use different accounts (or databases) for analytics connections and regular connections. A pooler reuses connections that have the same account and database. So if only the connection options differ, analytics session connections and regular session connections can get mixed (6.9, 11.5).
- To check, run
SHOW gritsurfer_session_mode;in an analytics connection through the pooler. It returnssurfer. In a regular connection, it returnspg.
6.6.1 Confirm that changes are applied · measure analytics session values
To confirm that changes are applied, check whether a change you just committed is visible in analytics sessions. In a regular session, write down the WAL position (pg_current_wal_lsn()), then check whether the unify checkpoint has passed that position.
SELECT pg_current_wal_lsn();
SELECT max(ckpt_lsn) >= '<value above>'::pg_lsn AS caught_up
FROM get_gritsurfer_unify_checkpoints();
- It is normal if it returns
t. If it returnsf, run it again after a few seconds. If it returnst, changes committed before then are visible in analytics sessions. Even without writes, the checkpoint passes that position, because GritDB commits an internal record every 10 seconds. - When you measure analytics session values, put
/*+ engine=burst, fallback=false */in front of the statement. Without it, if the GritDB engine cannot run the statement and PostgreSQL runs it instead, you still get the same value. If an ERROR occurs, check the registration status (6.5) first.
6.7 Operations not to perform on registered tables
GritDB 1.1 does not correctly apply the operations below to the GritDB engine when you run them on registered tables. Do not run them on registered tables. If you must, follow “What to do instead”.
| Operation | Impact | What to do instead |
|---|---|---|
TRUNCATE |
Deleted rows remain in analytics session results | Rebuild the table (below this table). You can skip keeping and reloading the data. For an analytics setup, see 6.4.2 |
| Add, drop, or rename columns | Analytics session queries fail, or new columns are not visible | Rebuild the table (below this table). For an analytics setup, see 6.4.2 |
| Change column types | Analytics session results can be wrong | Same |
Change REPLICA IDENTITY or SET UNLOGGED |
Changes may not reach the GritDB engine | Do not do it |
VACUUM FULL, CLUSTER, or SET TABLESPACE |
At the restart after an abnormal termination, some changes can be missing from the analytics data | Do not do it. To reclaim space, rebuild the table (below this table) |
| Attach or detach partitions | Rows go missing from or remain in analytics session results | Do not change partitions after you register all of them |
| Change the tablespace of a database that has registered tables | Changes may not be applied to the analytics data | After you unregister all tables |
Rename (RENAME or SET SCHEMA) |
The old name stays in the registration list (6.5). Use the new name when you unregister | Keep a record |
Rebuild the table (single-database setup):
- Stop writes to the table (applications and job schedules).
- Keep a copy of the data with
CREATE TABLE <s>.<t>_keep AS TABLE <s>.<t>;. - Compare the row count of the kept copy with the original. Run
SELECT (SELECT count(*) FROM <s>.<t>) = (SELECT count(*) FROM <s>.<t>_keep);and check that it returnst. - Unregister the table (6.3).
- Run
DROP TABLE <s>.<t>;.- First, check the dependent objects of the table with
SELECT DISTINCT pg_describe_object(classid, objid, 0) FROM pg_depend WHERE refclassid = 'pg_class'::regclass AND refobjid = '<s>.<t>'::regclass AND deptype = 'n';. - Record views and materialized views with
~/gritdb1/bin/pg_dump -p 5432 --schema-only --table=<view> <db> > <view>.sql(definition, privileges, and indexes), then drop them. For dependent views of a view, start from the top. - For foreign keys in other tables, record the result of
SELECT format('ALTER TABLE %s ADD CONSTRAINT %I %s;', conrelid::regclass, conname, pg_get_constraintdef(oid)) FROM pg_constraint WHERE contype = 'f' AND confrelid = '<s>.<t>'::regclass;and drop those constraints. Also stop writes to those tables from step 1. - Do not use
CASCADE. Re-create views in step 7 with~/gritdb1/bin/psql -p 5432 -d <db> -f <view>.sql. Re-create materialized views and foreign keys after the count comparison in step 8. Materialized views are createdWITH NO DATA, so continue throughREFRESH MATERIALIZED VIEW <view>;.
- First, check the dependent objects of the table with
- Run
CREATE TABLEwith the new definition (same primary key), then runALTER TABLE <s>.<t> REPLICA IDENTITY FULL;. - Register the table (6.3). Re-create the privileges and dependent views of the table.
- Reload the data with
INSERT INTO <s>.<t> SELECT … FROM <s>.<t>_keep;(6.8). Compare counts, then runDROP TABLE <s>.<t>_keep;.- If the row count exceeds the split threshold in 6.8 (half the limit, smaller for wide rows), split by primary key range and insert in several statements. Commit after each statement (example:
INSERT INTO <s>.<t> SELECT … FROM <s>.<t>_keep WHERE <key> >= <a> AND <key> < <b>;). - If there are
serialor identity columns, then forGENERATED ALWAYS AS IDENTITYcolumns, do step 8 withINSERT INTO <s>.<t> OVERRIDING SYSTEM VALUE SELECT … FROM <s>.<t>_keep;. Leave generated columns (GENERATED ALWAYS AS (…) STORED) out of the column list in step 8. Their values are computed again. Before step 9, set the sequence of each serial or identity column withSELECT setval(pg_get_serial_sequence('<s>.<t>', '<column>'), max(<column>)) FROM <s>.<t>;(Sequence functions,pg_get_serial_sequence).
- If the row count exceeds the split threshold in 6.8 (half the limit, smaller for wide rows), split by primary key range and insert in several statements. Commit after each statement (example:
- Reopen the connections that write to the table (6.3).
⚠ Do not run
DROP TABLEbefore you check the row count of the kept copy
- Reason: You cannot restore deleted rows.
- Instead: Go to step 5 only when the result of step 3 is
t.
- If you empty the same table and re-register it right away, registration can wait for the unregister cleanup and fail with an ERROR after 300 seconds. So register a newly created table.
- Register empty tables. If you register a table after you load data, those rows are missing from analytics session results (6.3, 10.4).
- The operations that you can run on registered tables are creating and dropping indexes,
VACUUM,ANALYZE, andCOPY(7.8).
6.8 Load data (bulk load)
-
Create an empty table (6.2 requirements and
REPLICA IDENTITY FULL). -
Register it (6.3).
-
Load the data (
COPYorINSERT … SELECT). One statement (oneCOPY, oneINSERT … SELECT, or one table in a dump file) is one transaction. So if the row count exceeds the split threshold (half the single-transaction limit below, smaller for wide rows), load in batches through a staging table.CREATE UNLOGGED TABLE <s>.<t>_load (LIKE <s>.<t>); \copy <s>.<t>_load FROM '<file>' (FORMAT csv) SELECT format('INSERT INTO <s>.<t> SELECT * FROM <s>.<t>_load WHERE <key> BETWEEN %L AND %L', min(<key>), max(<key>)) FROM (SELECT <key>, ntile(<batch count>) OVER (ORDER BY <key>) AS n FROM <s>.<t>_load) x GROUP BY n ORDER BY n \gexec- Run the commands above inside
~/gritdb1/bin/psql -p 5432 -d <db> -v ON_ERROR_STOP=1(it stops at the first error). - Do not register the staging table (
_load). It isUNLOGGED, so it writes little WAL. It needs as much extra space as the source data. - After an abnormal termination or a quick stop, the staging table remains, but its contents are emptied. In that case, start again from the
\copyline. Skip the batches already loaded. If<s>.<t>has rows, addWHERE <key> > (SELECT max(<key>) FROM <s>.<t>)afterFROM <s>.<t>_loadin the innerSELECTthat containsntile. Batches are committed one by one in key order. - If the file has a header line, run
\copywith(FORMAT csv, HEADER). It also reads line breaks inside CSV fields as they are, so do not split the file withsplit. <key>is one primary key column.<batch count>is the staging table row count ÷ the split threshold (rounded up).\gexecruns the generatedINSERTstatements one by one (a commit per statement).- Do not group several statements into one transaction. Do not load inside
BEGIN, withpsql -1, or withpg_restore --single-transaction. If you load from an application or a driver, turn on autocommit or commit after each batch (example: Python psycopg has autocommit off by default). - If there are generated columns or
GENERATED ALWAYSidentity columns, load as in step 8 of “Rebuild the table” in 6.7. - Commands:
\gexec,ON_ERROR_STOP, andntile
- Run the commands above inside
-
Collect statistics with
ANALYZE <s>.<t>;. -
Confirm that changes are applied (6.6.1).
-
Compare counts and sums between a regular session and an analytics session (the two queries in 6.4.1 step 9). If you used a staging table, they must also match its values (
SELECT count(*), sum(<numeric column>) FROM <s>.<t>_load;). If they match, runDROP TABLE <s>.<t>_load;.
- Check that all the values in step 6 are the same.
- Do not make registered tables
UNLOGGED, either when you create them or later. Changes may not reach the GritDB engine (6.7). - Each write to a registered table waits for an acknowledgment from the GritDB engine. So the same load is slower than on a table that is not registered (1.1).
Each server has a limit on the number of rows that one transaction writes to registered tables (INSERT, UPDATE, and DELETE combined). This is the single-transaction limit. GritDB 1.1 has no setting that changes this limit.
Check the limit (GritDB server, service account).
ls -tr ~/gritdb1/log/grit_unify_main.log* | xargs grep -h 'Tier-3 (RowVersion' | tail -1
Expected output (example):
[…][INFO][…] Tier-3 (RowVersion, 80%): max=18686611, initial=9343305, sizeof(PoolChunk)=512 + tuple=2229 = 2741
- The limit is the
max=value (in the example above, 18,686,611 rows on a server with 186 GiB of memory). It is proportional to server memory. - The GritDB engine writes this line when it starts. If there is no such line and the logs were deleted, check again after the next start.
- For one transaction, split it and commit below half the limit. Other transactions and changes that are not applied yet share the same limit. So do not run several large loads at the same time.
- If rows are wide (one row exceeds 2 KB), split into smaller batches by that ratio. For example, for 4 KB rows, split below one quarter of the limit. Measure the row size in the staging table (step 3; if there is none, the table that holds the data to load) as the decompressed size. Use
SELECT avg(octet_length(t::text))::int FROM <s>.<t>_load t;(for a large table,FROM <s>.<t>_load t TABLESAMPLE SYSTEM (1)). Long text andbyteacolumns come out at the decompressed size or more (this errs toward smaller batches). - In an analytics setup, the initial copy and operational database batch jobs use the same limit (warning in 6.4.1 step 8, 6.4.3). In the initial copy, one table is one transaction.
- For signs that a transaction is approaching the limit, see 7.1.
⚠ Do not write a transaction that exceeds the limit to registered tables
- Reason: GritDB stops (PostgreSQL included). That transaction is not applied.
- Instead: Split it and commit below half the limit. If you cannot split it, contact support (chapter 0).
During a load, watch the growth trend. For metrics, see 7.1. For thresholds, see 7.2.
| What to watch | How to check |
|---|---|
| Disk | df -h ~/gritdb1/datafiles/ ~/gritdb1/datafiles/pgdata/. If the two lines have the same Filesystem, it is one volume. If you installed with --pgdata, the second line is the pg_wal volume (2.4). If it fills up, see When the disk fills up |
pg_wal and buffer_spill size |
du -sh ~/gritdb1/datafiles/pgdata/pg_wal ~/gritdb1/datafiles/buffer_spill |
| WAL retained by the replication slot for recovery | SQL below |
| GritDB engine memory and approaching the single-transaction limit | The two rows in the table in 7.1 |
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots
WHERE slot_name = 'gritsurfer_slot';
For the initial copy in an analytics setup, attach large tables one at a time.
- Put only some of the tables in the first publication. Add the remaining tables one at a time after the initial copy of the previous table finishes (
srsubstateisr, 6.4.1 step 9). To add them, follow “Add a table to an existing analytics setup” in 6.4.2 (ALTER PUBLICATION … ADD TABLE→REFRESH PUBLICATION). - The number of tables that one subscription copies at the same time is the
max_sync_workers_per_subscriptionvalue (default 2). - To reduce the number of tables copied at the same time to one, run
ALTER SYSTEM SET max_sync_workers_per_subscription = 1;and thenSELECT pg_reload_conf();on GritDB (no restart, 5.4). When all initial copies showr, runALTER SYSTEM RESET max_sync_workers_per_subscription;and thenSELECT pg_reload_conf();. - Attach when the operational database is quiet (6.4.3).
6.9 Manage analytic queries
View running analytic queries. The functions are in A.3.
SELECT pid, status, query_start_time,
now() - query_start_time AS running, query_text
FROM get_gritsurfer_active_queries()
ORDER BY query_start_time;
- Long-running queries are the lines with a large
runningvalue.
Stop a query. <pid> is the pid in the result above.
| Order | SQL | When to use |
|---|---|---|
| 1 | SELECT pg_cancel_backend(<pid>); |
Cancel a running query |
| 2 | SELECT pg_terminate_backend(<pid>); |
When 1 does not end it, or for a query that waits for a GritDB engine execution slot. Cancel and statement_timeout do not work on such a query (6.6) |
- To check, run
SELECT pid, state FROM pg_stat_activity WHERE pid = <pid>;. If it returns 0 rows, the connection was terminated. If it returnsidle, only the query was canceled. - Functions and view:
pg_cancel_backendandpg_terminate_backend,pg_stat_activity - Do not terminate the
gritdb_catalog_*internal connections or GritDB workers (7.1).
Check wait time and run time with the stage times in the analytic query history. The query below shows 100 rows from the last hour.
SELECT query_text, status,
dispatched_time - query_start_time AS wait_admit,
worker_taken_time - dispatched_time AS wait_engine,
pg_end_time - query_start_time AS total
FROM get_gritsurfer_query_history(100, now() - interval '1 hour');
wait_admitis the time spent waiting for a GritDB engine execution slot.wait_engineis the time until the engine assigned a worker.totalis the total time.- Queries that waited past the time limit and ran on PostgreSQL have no row in the history (6.6 Concurrency).
- Concurrency is up to vCPUs × 4. Beyond that, queries wait and, after the time limit (5.6), run on PostgreSQL. Bind execution,
PREPARE, andfallback=falsestop with an error (6.6). - Keep separate accounts and connection pools for analytics sessions and for regular sessions. A pooler reuses connections that have the same account and database. For connection poolers, see 6.6. For privileges, see 11.5.
- Long analytic queries delay cold file cleanup, so do not let unfinished queries run for a long time (7.8).
- Resource limits for analytic queries (workload groups) are not available in 1.1 (10.6).
If a problem occurs
- If registration is rejected, see GDB-RUNTIME-001 in QuickStart, chapter 7, and the RUNTIME entries in TroubleShooting.
- If analytics session values differ from the regular session, first confirm that changes are applied (6.6.1). Then check whether an operation not to perform was run (6.7). In an analytics setup, see 6.4.2.
- If subscription apply stopped, see 6.4.2 and 7.5.
- If GritDB stopped during a large load or an initial copy, collect diagnostic data with QuickStart 7.4 before you start it again. Then contact support (6.8).
Learn more
- For monitoring, see chapter 7. To take the analytics setup again, see 8.6. For accounts and privileges, see 11.5. For limitations, see 10.4.
- PostgreSQL 17 documentation: Logical replication, Logical replication restrictions, Logical replication security,
CREATE SUBSCRIPTION, andEXPLAIN