Syncing data
This guide shows how to keep your own database in sync with the e-satisfaction BigQuery warehouse. It covers three jobs: a one-off full refresh to load everything, a daily incremental sync that pulls only what changed, and how to handle deletions so removed records don't linger downstream.
Who this is for
This is a developer-facing guide for the team building your sync. You'll need Data Warehouse access and the service-account credentials the e-satisfaction team provides.
Project & conventions
- Source dataset — your data lives in a dataset of the form
client-data-429110.client_<client_name>. Substitute your own dataset name throughout. - Time logic — there's no timezone handling; compare against
DATE(updated_time)andCURRENT_DATE(). - Keys & UPSERT — primary keys are marked in the schema documentation. The rule is simple: same key ⇒ update; otherwise insert.
- Tables — every table referenced here is listed in that schema documentation, which is your map for the full set.
Full refresh (one-off / rebaseline)
On the first run, load the full current state of each table you need. Every run after that can be an incremental sync (below). Optionally add AND organization_id = @organization_id to scope the data when your dataset spans multiple organizations.
-- Normalized responses (examples)
SELECT * FROM `client-data-429110.client_<client_name>.n_responses`;
SELECT * FROM `client-data-429110.client_<client_name>.d_responses_csat`;
SELECT * FROM `client-data-429110.client_<client_name>.d_responses_nps`;
SELECT * FROM `client-data-429110.client_<client_name>.d_responses_rating`;
-- Flat
SELECT * FROM `client-data-429110.client_<client_name>.f_instances`;
SELECT * FROM `client-data-429110.client_<client_name>.f_responses`;
-- Instances, configuration, queue & tags
SELECT * FROM `client-data-429110.client_<client_name>.n_questionnaire_instances`;
SELECT * FROM `client-data-429110.client_<client_name>.n_questionnaires`;
SELECT * FROM `client-data-429110.client_<client_name>.n_queue_items`;
SELECT * FROM `client-data-429110.client_<client_name>.n_tags`;
SELECT * FROM `client-data-429110.client_<client_name>.n_topics`;
Daily incremental
Every day, pull just the records that changed yesterday, so you sync only what's missing. You can widen the timeframe if a run failed or was skipped. All tables are partitioned on updated_time, so filtering on it keeps queries fast.
Declare yesterday once and reuse it:
DECLARE yesterday DATE DEFAULT DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY);
SELECT *
FROM `client-data-429110.client_<client_name>.n_responses`
WHERE DATE(updated_time) = yesterday;
-- ...repeat for the other tables in your scope
Consumer action: UPSERT each delta into your destination using the primary keys defined in the schema.
Handling deletions
Deletions are published to a ledger table. If an instance_id appears there, remove the related rows in your destination.
1. Fetch yesterday's deletions
DECLARE yesterday DATE DEFAULT DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY);
SELECT instance_id
FROM `client-data-429110.client_<client_name>.n_deleted_questionnaire_instances`
WHERE DATE(deleted_time) = yesterday;
2. Propagate the deletes
When an instance is deleted, the deletion must be propagated to its "child" tables. Delete rows matching each instance_id across:
n_responsesand alld_responses_*f_instances,f_responses, allf_responses_*, andf_sentimentn_questionnaire_instance_metadata
Preview the impact first
Before deleting downstream, you can preview how many rows each table would lose by joining your destination tables against the day's deleted instance_ids — a quick sanity check that the deletion is the size you expect.
Job template
A daily job comes together like this:
RUN_DATE = CURRENT_DATE()
YESTERDAY = RUN_DATE - 1 day
# 1) Pull deltas WHERE DATE(updated_time) = YESTERDAY for each table in scope
# 2) UPSERT into destination using natural keys (see Keys & UPSERT)
# 3) Deletions:
deleted_ids = SELECT instance_id
FROM n_deleted_questionnaire_instances
WHERE DATE(deleted_time) = YESTERDAY
FOR table IN [instances, responses*, flats, sentiment, metadata]:
DELETE FROM <dest.table> WHERE instance_id IN (deleted_ids)
# 4) Emit audit metrics (rows_upserted, rows_deleted, run_date, run_id)
The UPSERT step looks slightly different per destination — for example a MERGE in BigQuery or Snowflake, or an INSERT ... ON CONFLICT ... DO UPDATE in PostgreSQL — but the logic is the same: match on the natural keys, update on match, insert otherwise.
Validation & reconciliation
Validate that you received everything for a daily run:
DECLARE yesterday DATE DEFAULT DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY);
SELECT COUNT(*) AS changed_instances
FROM `client-data-429110.client_<client_name>.n_questionnaire_instances`
WHERE DATE(updated_time) = yesterday;
SELECT COUNT(*) AS deleted_instances
FROM `client-data-429110.client_<client_name>.n_deleted_questionnaire_instances`
WHERE DATE(deleted_time) = yesterday;
- Spot checks — sample a few
instance_ids from yesterday's pull and verify destination parity after UPSERT and DELETE. - Periodic parity — reconcile monthly counts by
organization_id,questionnaire_idandquestion_id.
Performance & reliability
- Always include
WHERE DATE(updated_time) = @yesterdayfor partition pruning. - Design idempotently: re-runs converge via UPSERT.
- For late arrivals, use a small lookback window — for example, the last three days rather than only yesterday.
- Keep destination columns nullable to absorb schema additions.
- Scope by tenant where applicable with
AND organization_id = @organization_id.
FAQ
Use d_responses_* or f_responses_*? Prefer the normalized d_responses_* / n_responses tables for analytics; the flat f_* tables are handy for simple ingestion.
Why "yesterday" and not "last N hours"? It guarantees stable, partition-pruned daily slices with simple scheduling.
How are deletions surfaced? Exclusively via n_deleted_questionnaire_instances; consumers must delete downstream by instance_id.
Further support
For help, contact [email protected] or open a ticket through the Support Portal.