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.
Why sync
Querying the warehouse directly is the quickest way to explore your data. Syncing it into your own database is the better fit when you want e-satisfaction feedback to sit next to your own data — joined with orders, CRM records or other sources in the tools your teams already use. A good sync stays small and predictable: load everything once, then each day pull only what changed and remove what was deleted.
Before you start
- 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.
Set up your sync
Run a 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. 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`;
Pull the 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
Then UPSERT each delta into your destination using the primary keys defined in the schema.
Fetch yesterday's deletions
Deletions are published to a ledger table. If an instance_id appears there, the related rows need to be removed from your destination.
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;
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
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.
Validate the run
Check that you received everything for the day:
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.
Putting it together: a daily job
A daily job combines steps 2 to 5 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.
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.
Keep it fast and safe to re-run
- 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.
Further support
For help, contact [email protected] or open a ticket through the Support Portal.