# Database Destinations

- URL: https://betteranalyst.com/docs/data-flows/database-destinations
- Updated: 2026-11-01
- Author: Better Analyst

Write Data Flow results to Snowflake, BigQuery, Databricks, PostgreSQL, Supabase, MySQL, SQL Server, or Amazon Redshift. Covers network access, required permissions, the tables Better Analyst manages, column types, and per-engine notes.

A database destination writes each run's result into a table you choose. This page covers what your database administrator needs to know before you point a production database at a flow.

Database destinations are available on Pro, Team, and Enterprise plans for Snowflake, BigQuery, Databricks, PostgreSQL, Supabase, MySQL, SQL Server, and Amazon Redshift.

## Before You Connect

You need four things:

1. **Network access** from Better Analyst to your database (see below).
2. **A dedicated database user** for Better Analyst, with write permission on one schema.
3. **A schema that already exists.** Better Analyst creates tables, but not schemas. Create the target schema first, or use the **Create schema** action on the connector page.
4. **For BigQuery, a service-account connection.** A BigQuery connection created with OAuth cannot be used as a destination.

## Network Access

Better Analyst connects to your database from its own servers over the public internet, so the database must accept inbound connections from those addresses.

Most managed databases block all external traffic by default. You will typically need to:

- **Add Better Analyst's addresses to your allowlist** — the security group, firewall rule, or IP allowlist that governs your database. In Amazon RDS this is the security group's inbound rules; in Google Cloud SQL it is **Authorized networks**; in Azure SQL it is **Firewall rules**; in Supabase it is **Network Restrictions**.
- **Enable SSL/TLS.** Connections are encrypted in transit. Snowflake, BigQuery, and Redshift require it; for PostgreSQL, MySQL, and SQL Server, enable it on the server and select SSL in the connector.
- **Open the database's port** to those addresses — 5432 for PostgreSQL and Supabase, 3306 for MySQL, 1433 for SQL Server, 5439 for Redshift. Snowflake, BigQuery, and Databricks are reached over HTTPS and need no port changes.

Better Analyst connects from these address ranges. Allowlist both:

```
74.220.50.0/24
74.220.58.0/24
```

These ranges are stable, so this is a one-time configuration step. If your security policy requires individual addresses rather than CIDR ranges, contact support.

A database that is only reachable inside a private network — a VPC with no public endpoint, or a host on `localhost` — cannot be used as a Data Flow destination. Expose it through a public endpoint restricted to the allowlist, or use a bastion or tunnel that provides one.

## Required Permissions

Create a dedicated user rather than reusing an application or admin account. Better Analyst needs, **scoped to the one target schema**:

| Permission | Why it is needed |
| - | - |
| `CREATE TABLE` | Creates the destination table on the first run, plus temporary staging tables |
| `INSERT` | Writes the rows |
| `UPDATE`, `DELETE` | Applies **Update or add rows** and **Replace with latest data** |
| `SELECT` | Reads back its own bookkeeping and confirms row counts |
| `DROP` (own tables) | Cleans up its temporary staging tables after each run |

A PostgreSQL example:

```sql
CREATE USER formulabot_writer WITH PASSWORD 'your-secure-password';
GRANT USAGE, CREATE ON SCHEMA analytics TO formulabot_writer;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA analytics TO formulabot_writer;
ALTER DEFAULT PRIVILEGES IN SCHEMA analytics
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO formulabot_writer;
```

Grant these on a schema dedicated to Better Analyst deliveries — not on a schema holding tables your applications depend on. **Replace with latest data** deletes existing rows in its target table by design.

Better Analyst refuses to write to system schemas regardless of permissions, including `information_schema`, `pg_catalog`, `mysql`, `sys`, `master`, `msdb`, and `tempdb`.

## Tables Better Analyst Creates

Alongside your destination table, Better Analyst maintains its own tables in the same schema. Every one begins with `_fb_`:

| Table | Purpose |
| - | - |
| `_fb_sync_meta` | One row per destination table, recording the last run's ID, write mode, rows inserted and updated, and final row count |
| `_fb_stg_…` | Short-lived staging tables used while a run is being applied |
| `_fb_write_probe_…` | Created and removed when a destination is first tested, to confirm write access |

`_fb_sync_meta` is what makes a run safe to retry. If a run is interrupted midway — a network drop, a restart — the next attempt reads this table, recognizes the interrupted run, and finishes or rolls it back rather than writing the same rows twice.

Leave `_fb_` tables in place. Deleting `_fb_sync_meta` does not break the destination table, but the next run loses its safety record and re-creates it from scratch.

## Column Types

Better Analyst maps every delivered column to one of three general types:

| Delivered as | PostgreSQL / Supabase | MySQL | SQL Server | Snowflake | BigQuery | Redshift | Databricks |
| - | - | - | - | - | - | - | - |
| Text | `text` | `TEXT` | `NVARCHAR(MAX)` | `STRING` | `STRING` | `VARCHAR(65535)` | `STRING` |
| Number | `double precision` | `DOUBLE` | `FLOAT` | `DOUBLE` | `FLOAT64` | `DOUBLE PRECISION` | `DOUBLE` |
| Boolean | `boolean` | `TINYINT(1)` | `BIT` | `BOOLEAN` | `BOOL` | `BOOLEAN` | `BOOLEAN` |

Two consequences worth planning around:

- **Dates arrive as text.** A `date` column lands as a string, so `WHERE date >= '2026-08-01'` compares text rather than dates. It works for `YYYY-MM-DD` values because they sort correctly as text, but it is not a date column.
- **Whole numbers arrive as floating point.** A count of 3,788 may display as `3788.0`.

If you need stricter types, create a view over the destination table that casts the columns, and point your reports at the view. Better Analyst only ever writes to the table it manages, so a view on top is never disturbed by a run.

A text column used as a **merge key** is created narrower so the database can index it — `VARCHAR(255)` in MySQL and `NVARCHAR(255)` in SQL Server, since neither engine can index an unbounded text column. Merge-key values longer than 255 characters fail the write rather than being truncated.

Existing columns are not retyped. If you pre-create the destination table with the types you want and the delivered data fits, Better Analyst writes into your types.

## Table and Column Naming

Table, schema, and column names must start with a letter or underscore, contain only letters, numbers, and underscores, and be 64 characters or shorter. Names beginning with `_fb_` are reserved.

If your source data has column names that do not fit — spaces, punctuation, or leading digits — rename them in the transform step before delivery.

## Size Limits

| Limit | Value |
| - | - |
| Rows written per run | 500,000 |
| Columns per table | 512 |
| Total cells per run | 5,000,000 |
| Time for a single statement | 120 seconds |

In practice the [source limits](https://betteranalyst.com/docs/data-flows/sources) bind first: a run reads at most 100,000 rows from its source, so a delivery only approaches 500,000 rows if a transform multiplies rows — for example a join that fans out.

If a run exceeds the statement timeout, the usual cause is a destination table without an index on its merge-key columns. See below.

## Write Modes on a Database

Database destinations support the same three write modes as Google Sheets:

| Mode | What it does to the table |
| - | - |
| Update or add rows | Rows whose merge keys match are updated in place; new keys are inserted; rows absent from this run are left alone |
| Replace with latest data | All existing rows are deleted, then this run's rows are inserted |
| Append every run | This run's rows are inserted; nothing existing is touched, and duplicates are possible |

**Index your merge keys.** For **Update or add rows**, add a unique index or constraint on the merge-key columns of the destination table. Without one, each run scans the whole table to find matches, which gets slower as the table grows and eventually hits the statement timeout:

```sql
CREATE UNIQUE INDEX idx_daily_sessions_key
  ON analytics.daily_sessions (date, campaign_id);
```

**Replace with latest data** deletes every existing row in the destination table on every run, including rows an earlier run wrote and rows you added by hand. Use a table dedicated to this flow.

## Per-Engine Notes

### PostgreSQL

PostgreSQL 12 or later. The writer role needs `USAGE` and `CREATE` on the target schema, as in the example above.

### Supabase

Supabase uses the PostgreSQL connection path with the **Session pooler** details from the Supabase **Connect** dialog, the same connection used for reading — see [Connect Supabase](https://betteranalyst.com/docs/connectors/supabase). If the target schema has row-level security enabled, confirm the writer role's policies permit insert, update, and delete, or the run fails with a permission error even though the grants look correct.

### MySQL

MySQL 5.7 or later, on port 3306.

### SQL Server

SQL Server 2016 or later, and Azure SQL. The connection field is the **server** name; a named instance goes in the instance field rather than being appended to the server name.

### Snowflake

Requires a warehouse, database, and schema. The role must have `USAGE` on the warehouse and `CREATE TABLE` on the schema. A suspended warehouse is fine if it is set to auto-resume; if it cannot resume, runs fail while it is suspended.

### Amazon Redshift

Provisioned clusters and Redshift Serverless, on port 5439. The user needs `CREATE` on the target schema. Schema and table names must be lowercase. Redshift `VARCHAR` lengths are in bytes, so a text value longer than 65,535 bytes fails the write rather than being truncated. Replace runs delete and re-insert inside one transaction (Redshift's `TRUNCATE` cannot be rolled back), and merge-key upserts use `MERGE`.

### Databricks

Requires a Unity Catalog catalog and a SQL warehouse. The destination principal needs `USE CATALOG` on the catalog and `USE SCHEMA`, `CREATE TABLE`, `MODIFY`, and `SELECT` on the target schema. Tables are created as managed Delta tables; replace, append, and merge-on-key runs use Delta `CREATE OR REPLACE TABLE`, `INSERT`, and `MERGE INTO`.

Use a separate Databricks connection (a different service principal) for writing. Protected reads refuse a principal that holds write or ownership privileges, so a connection set up for destination writes cannot also be used as a chat, dashboard, or Data Flow source.

### BigQuery

Requires a service-account connection — an OAuth-based BigQuery connection cannot be a destination. The service account needs `BigQuery Data Editor` on the target dataset and `BigQuery Job User` on the project. The dataset must already exist, and its location is fixed at creation.

## Troubleshooting

### "The database identity could not be verified"

Better Analyst checks that the database it is about to write to is the one the flow was configured against. This appears when the connector's host, port, or database name no longer resolves to the same server — usually after a database was moved, restored, or recreated. Reconnect the connector with the current details, then re-save the flow.

### Connection timeouts

The allowlist is the usual cause. Confirm the current Better Analyst addresses are in the database's firewall rule, and that the rule covers the port your database listens on.

### "Permission denied" on the first run

The first run creates the destination table, which needs `CREATE TABLE` on the schema — a broader permission than the `INSERT` needed by later runs. A user granted only `INSERT` connects and tests successfully, then fails on the first real delivery.

### Runs slow down as the table grows

Add a unique index on the merge-key columns, as above.

## Next Steps

- [Source types and limits](https://betteranalyst.com/docs/data-flows/sources)
- [Destinations and write modes](https://betteranalyst.com/docs/data-flows/destinations)
- [Schedules and runs](https://betteranalyst.com/docs/data-flows/schedules-and-runs)
