Last updated
One way: SharePoint stays the system of record, and SPSync keeps a read-only copy of your lists current in ClickHouse.
Status: Shipped · product: SPSync for Warehouses & Lakes
ClickHouse scans and aggregates large tables quickly, which makes it a good home for SharePoint lists that have grown into years of operational history. A Grafana panel that counts incidents by month, or a Metabase question over every approval since the list was created, returns in a moment, and the list data can sit beside the event and log data your ClickHouse already holds.
Connecting, and the two ports
The connection string looks like
clickhouse://user:password@host:9440/database?secure=1 and lives in an environment
variable. ClickHouse uses two ports:
- Native protocol. 9440 with TLS, as on ClickHouse Cloud. A self-hosted server
without TLS in front of it uses 9000 with
secure=0. - HTTP, for loading and mutations. 8443 by default, matching ClickHouse Cloud. A
self-hosted server normally listens on 8123; add
&http_port=8123to the string.
ClickHouse has databases but no schemas, so the destination’s dataset name becomes a
table-name prefix. With a dataset name of sync, a list table named ops_incidents is
created as sync___ops_incidents, and several replications can share one database.
How rows are written
Each list becomes a plain MergeTree table. A run upserts changed rows by deleting the old
version and inserting the new one, so the table always holds one current row per item and
a query needs no FINAL modifier. Multi-value columns get child tables, one row per item
and value, and every person column points at the shared sp_users table.
Soft deletes use ClickHouse’s ALTER TABLE ... UPDATE mutation with mutations_sync = 1,
so the deletion is visible when the run ends. ClickHouse queues mutations per table and
starts refusing writes when too many are pending; SPSync issues at most one per table per
run, which matters only for a table with constant deletion churn.
Text is ClickHouse’s unbounded String, timestamps are DateTime64(6, 'UTC'), and
JSON-shaped values such as a location column’s raw form are stored as String. A column
added in SharePoint appears on the next run, a removed one stays, and a retyped column
gets a _v2 column beside it.
| Title | Client lookup | Tags multi-choice | Owner person | Due |
|---|---|---|---|---|
| Riverside | Acme Ltd | Design; Build | Ada Lovelace | 2026-11-30 |
| Harbour | Borealis plc | Build | Alan Turing | 2027-02-14 |
then sync
- id
- title
- client_id → clients.id
- owner_id → sp_users.id
- due
- is_deleted soft delete
- sp_modified
- id
- title
- …
- project_id
- tag
One row per value: Riverside has two.
- id
- display_name
One users table shared by every list.
Where it helps
Dashboards over long histories. Grafana or Metabase panels over years of list data; see Tableau, Looker, Metabase and Grafana on SharePoint data.
Operational screens. A wall dashboard of open incidents updates within a few minutes on the default five-minute schedule; see operational dashboards.
A warehouse source. SharePoint lists sit beside other ingested data under the same access model; see SharePoint as a governed warehouse source.
dbt. The key columns give dbt a stable source to model from; see dbt models over replicated SharePoint tables.
Setting it up
- Authorise SharePoint access with delegated sign-in, or per-site
Sites.Selectedapp permissions for a sync that runs without a user. - Create a ClickHouse user that can create tables, and views if you want them.
- Store the connection string in an environment variable.
- Choose the lists or a whole site and run. Turn on reporting views if your BI tool prefers a pre-joined shape with deleted rows already filtered out.
- Set a schedule, every few minutes up to hourly.
ClickHouse does not enforce keys, so SPSync’s upserts are what keep one row per item. SPSync copies list and library metadata, never file contents, and never writes to SharePoint.
Watch it run
Chapters are in the video's description.
Watch the walkthrough: the Northwind Analytics demo lists in SharePoint, connecting to Microsoft 365 in the desktop app, picking lists, the run into an empty ClickHouse server, and the replicated tables and analytics queries in ClickHouse Play.
Watch the command-line version of this walkthrough.
Desktop, Azure or command line
All three editions run the same engine, write the same tables and use the same licence key, so the choice comes down to who looks after the sync.
Desktop app
For macOS and Windows. Sign in with Microsoft, tick the lists, choose a database and sync from your own machine on the schedule you set. Delegated sign-in needs no admin unless your tenant requires admin consent for apps.
In your Azure
SPSync in your Azure, a managed application for your own subscription, is deployed with you on a call while the Marketplace listing is prepared. Once deployed it is always on, so the sync keeps running with every laptop closed.
Command line
Every replication is a YAML file you can review and commit. Run it from a server, a container or a CI pipeline, and re-run it safely any time.
Frequently asked questions
Which table engine does SPSync use in ClickHouse?
A plain MergeTree table per list. Each run upserts changed rows with a delete followed by an insert, so a query on the table already sees one current row per item, with no FINAL modifier and no wait for background merges.
How are multi-choice columns stored?
As child tables, one row per item and value, the same shape SPSync writes to every other destination, never as ClickHouse Array columns. Multi-lookup and multi-person columns use the same shape and are still being checked on a live tenant.
How are deleted SharePoint items handled?
The row stays and its sp_deleted_at column is set, using a ClickHouse mutation that completes before the run finishes. A query, or the optional reporting view, filters those rows out.
Which ports does the connection need?
Two. The native protocol port, 9440 with TLS on ClickHouse Cloud, and the HTTP port used for loading, 8443 on Cloud. A self-hosted server usually listens for HTTP on 8123, which you set as http_port in the connection string.
Is there a free trial for the ClickHouse destination?
ClickHouse is sold after a call. Get in touch and we will show it against a sample of your own lists.
Talk to us about ClickHouse
SPSync is licensed per installation and per target family, with Standard Support included. Tell us which targets you need and how many machines, and we will size it with you. See how licensing works.
Set it up
Sign in to SharePoint with the SharePoint connection guide, then follow the ClickHouse setup guide to create the database user and open the connection for ClickHouse. If the connection test fails, the troubleshooting guide lists the errors people hit and what fixes them.
Script it
Everything you can click, you can script. The CLI, YAML config and automation below reproduce this scenario exactly. See the CLI reference.
spsync auth login --client-id 00000000-0000-0000-0000-000000000000
spsync export-config --output replication.yaml --pipeline-name clickhouse --prefix clickhouse --destination-kind clickhouse --connection-string-env SPSYNC_DB_CONNECTION --dataset-name clickhouse --client-id 00000000-0000-0000-0000-000000000000 --site-url https://contoso.sharepoint.com/sites/example --all-lists
spsync doctor replication.yaml
spsync run replication.yaml pipeline_name: clickhouse
prefix: clickhouse
auth:
mode: delegated
client_id: 00000000-0000-0000-0000-000000000000
tenant_id: common
destination:
kind: clickhouse
connection_string_env: SPSYNC_DB_CONNECTION
dataset_name: clickhouse
scopes:
- site_url: https://contoso.sharepoint.com/sites/example
mode: entire_site
reconcile_interval_days: 7
create_reporting_views: false
Cron (ClickHouse nightly):
# every night at 02:00
0 2 * * * cd /opt/spsync && SPSYNC_DB_CONNECTION="$(cat /etc/spsync/db.env)" uv run spsync run replication.yaml >> /var/log/spsync/run.log 2>&1 GitHub Actions:
# .github/workflows/spsync-run.yml
name: SPSync
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch: {}
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync
- run: uv run spsync run replication.yaml
env:
SPSYNC_DB_CONNECTION: ${{ secrets.SPSYNC_DB_CONNECTION }}
SPSYNC_LICENCE_KEY: ${{ secrets.SPSYNC_LICENCE_KEY }} Azure DevOps:
# azure-pipelines.yml
schedules:
- cron: "0 2 * * *"
branches: { include: [main] }
always: true
steps:
- script: uv sync
- script: uv run spsync run replication.yaml
env:
SPSYNC_DB_CONNECTION: $(SPSYNC_DB_CONNECTION)
SPSYNC_LICENCE_KEY: $(SPSYNC_LICENCE_KEY) Windows (PowerShell + Task Scheduler):
# Windows Task Scheduler action (see /docs/cli/automation/)
$env:SPSYNC_DB_CONNECTION = (Get-Content C:\spsync\db.env)
uv run spsync run C:\spsync\replication.yaml
schtasks /Create /SC DAILY /ST 02:00 /TN "SPSync" /TR "powershell -File C:\spsync\run.ps1" macOS (launchd):
# ~/Library/LaunchAgents/io.spsync.run.plist, then: launchctl load ~/Library/LaunchAgents/io.spsync.run.plist Secrets (licence key, connection strings) come from the environment, so never write them into the YAML. See scheduling and secrets.