The SharePoint List connector in Power BI pages through the REST API one item at a time, so a report that refreshed in seconds when the list was small takes minutes once it has grown. This walkthrough moves a reporting team’s Projects, Tasks and Departments lists into SQL Server with SPSync and points the same report at the tables with DirectQuery. The video above follows the same steps on the desktop app.
The symptoms will be familiar to anyone who has kept a report on a list for a year. Each lookup, person or choice column arrives as an id the report has to resolve. Once a list passes 5,000 items, a query that filters or sorts on a non-indexed column can fail (what breaks past 5,000 items has the detail). A Power Automate flow copying rows across holds until someone changes a lookup or a multi-value column. Data Factory or a scheduled copy pipeline can do the job, but its schema and mapping have to be maintained by hand, and plenty of teams fall back to a one-off export into SQL Server or a spreadsheet.
What the connector does that a replica avoids
SharePoint’s list APIs are designed for SharePoint pages. A BI tool that pulls a whole list on a timer is a poor fit for them. SPSync reads the list into SQL Server once, then keeps the table current with delta queries, so Power BI, Excel and report writers query SQL Server and leave SharePoint alone. SPSync builds the schema itself: lookups and people become key columns, multi-value columns become child tables, and each run after the first reads only the items that changed.
Installing SPSync and signing in
Install the desktop app, or SPSync in your Azure for a tenant-wide setup. Sign in with the
account that already opens Projects, Tasks and Departments in SharePoint Online; the
sign-in is delegated, so SPSync reaches those lists and nothing beyond what that account
can open. The permissions wizard explains what Sites.Selected grants before an admin
approves it, and SPSync never writes to SharePoint. On the command line, sign-in is a device
code and the token sits in the OS keychain, so a scheduled job stores no password. The
command-line walkthrough runs this scenario end to end from
a terminal.
Lookups, choices and people in SQL Server
Each list becomes a table keyed on the SharePoint item id, which is how later runs find the
row to update. A single-value lookup becomes a key column named after the lookup, such as
department_id on sp_projects. A multi-value lookup or choice becomes a child table with
one row per value per item; Assigned To on Tasks becomes sp_tasks__assigned_to. Person
columns from all three lists resolve against one shared users table, so “assigned to” and
“created by” join the same way.
When a SharePoint column changes type, for example a number field becoming text, SPSync adds a column for the new type and keeps the old values as they were. A column deleted in SharePoint stays in the table, so a visual built on it still renders the next morning.
Pointing Power BI at the tables
With the lists in SQL Server, Power BI connects as it would to any database: import for a scheduled model, or DirectQuery for something close to live. DirectQuery sends filtered queries to SQL Server, which the SharePoint List connector cannot do at any useful size. Because the department lookup is now a column, you define the relationship once in the model and the report joins projects to departments like any two tables. The merge step that resolved lookups at refresh time goes away.
A five-minute schedule
Set the interval to five minutes and each run reads only what changed, keeping the report a
few minutes behind SharePoint at most. A full reconcile, every seven days by default,
re-checks every item; you can also run one on demand as a QA check, or after an outage on
your own server or in Microsoft 365. The desktop app and SPSync in your Azure both have a
schedule picker. From the command line, spsync run does one run, and cron, a scheduled
task or a CI pipeline sets the pace.
Edited and deleted items
An edited item is updated by the next delta run, without a reload. A deleted item is soft-deleted: the row stays with a flag, so a report can filter it out or keep it for an audit, and the reporting view SPSync can generate for each table already leaves it out.
The tabs under each step show the same setup in the desktop app, in SPSync in your Azure and on the command line.
Connect to SharePoint and sign in
What you see: The connect wizard asking for the reporting site URL and a delegated sign-in, then the permissions wizard's one-page pack.
Why it matters: The report's data source runs under the reporting team's own SharePoint rights, so IT approves one site and no tenant-wide app.
What it replaces: A gateway service account with read access across the whole tenant.
Pick the lists and the SQL Server destination
What you see: Projects, Tasks and Departments ticked together, mapped onto a new SQL Server schema with a preview of every column and type.
Why it matters: Tasks and projects both point at Departments, so one replication of all three keeps every lookup resolvable.
What it replaces: A hand-written ETL script or a Power Automate flow per list, each one breaking separately when a column changes.
Tick Projects, Tasks and Departments, then choose SQL Server as the destination and point it at the target database.
Check the schema preview: every column, its type, and which columns are lookups before anything runs.
Write the replication config for all three lists against SQL Server in one file.
spsync export-config --output reporting.yaml --pipeline-name reporting --prefix sp --destination-kind mssql --dataset-name dbo --client-id $SPSYNC_CLIENT_ID --connection-string-env REPORTING_DB_URL --site-url https://contoso.sharepoint.com/sites/reporting --lists-by-name Projects,Tasks,DepartmentsCheck the Graph permissions and the SQL Server connection before the first run.
spsync doctor reporting.yaml
Run the first sync
What you see: 148 projects, 121 tasks and 34 departments written in a single run, with a delta run after that.
Why it matters: Later runs fetch only changed items, so the five-minute schedule costs a handful of Graph calls per run.
What it replaces: A scheduled Power BI refresh that times out against the SharePoint List connector.
Click Run now and watch the per-list row counts arrive: 148 projects, 121 tasks, 34 departments.
Set the schedule to five minutes so the three-list replication checks for changes twelve times an hour.
Run the replication; the terminal shows the full load first, then delta counts on later runs.
spsync run reporting.yaml
See the result: lookups, child tables and joins
What you see: sp_projects joined to sp_departments in a SQL query, and a Priority column added to sp_tasks automatically after a change in SharePoint.
Why it matters: Workload by department is a join and a count, which SQL Server answers in one query and a list view cannot.
What it replaces: An export to a spreadsheet every time a report needs a join SharePoint cannot answer on its own.
Open the destination database and browse sp_projects, sp_tasks and sp_departments; each keeps the SharePoint item id as its key.
Run a join between sp_projects and sp_departments on department_id, something no SharePoint list view answers directly.
Query the SQL Server database directly to confirm the join and the row counts.
sqlcmd -S your-server.database.windows.net -d reporting -Q "select p.title, d.title from dbo.sp_projects p join dbo.sp_departments d on p.department_id = d.sp_item_id"
Connect Power BI with DirectQuery
What you see: Power BI reading sp_projects straight from SQL Server with DirectQuery, joined to departments on department_id.
Why it matters: Filters and slicers become SQL that SQL Server runs, with no per-item API paging and no 5,000-item view threshold.
What it replaces: The SharePoint List connector's scheduled refresh, which times out or falls back to import once the list grows large.
In Power BI Desktop, connect to the SQL Server database and choose DirectQuery for sp_projects.
Build the report against the department column the same way you would against any other database table, and define the relationship once in the model.
Keep the replication current from a script or CI job, independent of when the report is opened.
spsync run reporting.yaml