Joining lists on lookups the way SharePoint cannot

Join related SharePoint lists in SQL: SPSync turns every lookup into a key column pointing at the replicated table it refers to.

Book a call

Book a call to talk through your use case, see further walkthroughs or demos, and set up a trial in your own environment.

Last updated

A report needs data from three related lists joined on their lookup columns, and SharePoint has no way to do that without pulling everything into Power Automate or Excel by hand.

SharePoint lookups go one field deep. A Tasks list can show the title of the project each task belongs to, but it cannot show that project’s budget, or the department of the person the task is assigned to, because SharePoint has no server-side join across lists. A report that needs Projects, Tasks and People together ends up as a Power Automate flow that loops over records, a Power Query merge that breaks when a column changes, or a script that pulls all three lists through Graph and joins them in memory on every run.

Lookups become key columns

SPSync replicates each list into its own table and turns every lookup into a key column holding the ID of the item it points at, alongside its display value. Tasks to projects to people is then a plain SQL JOIN. After the first load each run reads only what changed, so the joined result stays current without fetching anything twice.

The details matter once you write the query. A single-value lookup is a column on the row. A multi-choice column is a child table with one row per value back to the parent, and multi-value lookups follow that shape once confirmed against a live tenant. Every person column, in every list, resolves against one shared sp_users table, so “assigned to” joins the same way in Tasks as in Projects. A report that needs more than SharePoint’s 12 lookups in one query works the same way; the post on the lookup column threshold shows where that limit stops views, flows, apps and Power BI.

A lookup column becomes a joinable id column, so the join is one line of SQL.
A lookup column becomes a joinable id column, so the join is one line of SQL.

Bring both ends of every lookup

A join needs a table on each side. Select every list involved, including the ones the others look up into, and send them to the same destination database on one schedule so the tables stay in step. Leave a list out and its lookup columns still replicate, with nothing on the other end to join to.

Deleted rows keep their place with sp_deleted_at set, so a join still works against history unless the query filters them out. For a head start, SPSync can generate a reporting view per list that adds person names and leaves out deleted rows, and the cross-list report becomes a short query over those views.

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.

Set it up

Ready to run this against your own site? The setup guide takes you from install to a first replication, and the SharePoint connection guide covers signing in.

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 cross_list_joins --prefix cross_list_joins --destination-kind postgres --connection-string-env SPSYNC_DB_CONNECTION --dataset-name cross_list_joins --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: cross_list_joins
prefix: cross_list_joins
auth:
  mode: delegated
  client_id: 00000000-0000-0000-0000-000000000000
  tenant_id: common
destination:
  kind: postgres
  connection_string_env: SPSYNC_DB_CONNECTION
  dataset_name: cross_list_joins
scopes:
  - site_url: https://contoso.sharepoint.com/sites/example
    mode: entire_site
reconcile_interval_days: 7
create_reporting_views: false

Cron (PostgreSQL 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.

Related reading: Column type mapping, with links to the articles and docs that cover it.

Book a call

Book a call to talk through your use case, see further walkthroughs or demos, and set up a trial in your own environment.