
Pitfalls When Using Linked Server
An interesting project related to processing the task queue came to our company. The project was previously developed by another team. We needed to deal with the problems that arise with a large load on the queue, and, accordingly, fix the problems found.
In short, the project consists of several databases and applications located on different servers. The “task” in this project is a stored procedure or .Net application. Accordingly, the "task" must be performed on a specific database and on a specific server.

All data related to the queue is stored on a dedicated server. On servers where you need to perform tasks, only metadata is stored, i.e. procedures, functions, and service data that relate to this server. Accordingly, the data related to the tasks we receive queries using LinkedServer.
All data related to the queue is stored on a dedicated server. On servers where you need to perform tasks, only metadata is stored, i.e. procedures, functions, and service data that relate to this server. Accordingly, the data related to the tasks we receive queries using LinkedServer.
The following are the two most popular classic ways to process a queue:
Initially, the project implemented the second option. To minimize the latency of processing tasks, our application polls the queue every 100-500ms.
Actually, there is nothing wrong with this, except for one thing - with this implementation, the table is once again blocked. I will say in advance that the query uses row locking with the ability to only read unlocked rows:
So, back to the problem. In the analysis, I noticed the value of the counter - batch requests / sec in Active Monitor. This value, with a small number (about 50) of tasks in the queue, went off scale for 1000, and the CPU load increased sharply.
First thought: you need to proceed to the implementation of the first option (sending a notification to the task handler). This method was implemented using Service Broker and SignalR :
This tool is already used in the project, and the deadlines were tightened, so I did not implement something similar, for example, NServiceBus .
My surprise knew no bounds when this solution did not help. Yes, there was a gain in productivity, but this did not solve the problem completely. A stress test was written for debugging, when more than 500 tasks are added to the queue.
The creation of such a stress test made it possible to find the " root of evil ."
Analysis of the list of active queries and performance reports, during a heavy load of showing the presence of "very interesting queries", which consisted of one command:
Further analysis revealed that these are queries from LinkedServer. The question immediately arose: “Is it really a request of this type select * from RemoteServer.RemoteDatabase.dbo.RemoteTable where FieldId = Value generates a request (fetch api_cursor0000000000000003) to RemoteServer?” It turns out, yes, and even when LinkedServer is MS SQL.
For a more illustrative example, create the table “Test” (the table creation code is available in the appendix to the article) on server “A”, and on server “B” we will execute the request:
where dev2 is our server “A".
At the first execution of such a request, we will have a similar log in the profiler on server A:
And now we’ll execute the requests by ID:
As you can see in the screenshot, the request plan was added to the cache. If you run this request a second time, then a little better.
As we can see, the data is already taken from the cache.
When conditions change, we get a similar sample - the first sample for a given Id . But the bottom line is that with large numbers of different requests, the cache is not enough. And sql starts to make a bunch of queries to the table, which leads to "brakes". You ask: “What about the indices?” There are indexes, but even queries with a Primary Key (PK) condition caused this problem.
What does Google say about this? And a lot of things, but no sense:
More sensible answers were found only in 3 articles:
As far as I understand, you cannot configure LinkedServer so that Pull technology is always used to receive data from LinkedServer. It all depends on where you are processing the request.
Time was running out, and the only solution that could save us was to rewrite some of the queries in dynamic sql. Those. execute requests on the server on which the data is stored.
There are several ways to work with data on LinkedServer:
The fourth method was used to solve the problem.
Below are some graphs of incoming and outgoing traffic on the server where the main queue base is located before and after using sp_executesql. The size of the database is 200-300Mb.
Outgoing peaks are copying backup to NFS.
The conclusion suggests itself: initially, the driver from MS for working with "MS sql linked server" cannot itself execute requests on the data source server. Therefore, colleagues, let's try to execute them at the data source, in order to solve at least part of the performance issues.
Files for the article.
In short, the project consists of several databases and applications located on different servers. The “task” in this project is a stored procedure or .Net application. Accordingly, the "task" must be performed on a specific database and on a specific server.

All data related to the queue is stored on a dedicated server. On servers where you need to perform tasks, only metadata is stored, i.e. procedures, functions, and service data that relate to this server. Accordingly, the data related to the tasks we receive queries using LinkedServer.
All data related to the queue is stored on a dedicated server. On servers where you need to perform tasks, only metadata is stored, i.e. procedures, functions, and service data that relate to this server. Accordingly, the data related to the tasks we receive queries using LinkedServer.
Why is that?
- Convenience. We can indicate at any time that data is now stored on server B.
- So it was realized before us.
The following are the two most popular classic ways to process a queue:
- Send a notification to the task handler about the presence of a task.
- Poll the queue for tasks.
Initially, the project implemented the second option. To minimize the latency of processing tasks, our application polls the queue every 100-500ms.
Actually, there is nothing wrong with this, except for one thing - with this implementation, the table is once again blocked. I will say in advance that the query uses row locking with the ability to only read unlocked rows:
READPAST, ROWLOCK, UPDLOCK
So, back to the problem. In the analysis, I noticed the value of the counter - batch requests / sec in Active Monitor. This value, with a small number (about 50) of tasks in the queue, went off scale for 1000, and the CPU load increased sharply.
First thought: you need to proceed to the implementation of the first option (sending a notification to the task handler). This method was implemented using Service Broker and SignalR :
- Service Broker was used to send notifications about the appearance of a task;
- SignalR was used to send notifications to task handlers.
Why SignalR?
This tool is already used in the project, and the deadlines were tightened, so I did not implement something similar, for example, NServiceBus .
My surprise knew no bounds when this solution did not help. Yes, there was a gain in productivity, but this did not solve the problem completely. A stress test was written for debugging, when more than 500 tasks are added to the queue.
The creation of such a stress test made it possible to find the " root of evil ."
Analysis of the list of active queries and performance reports, during a heavy load of showing the presence of "very interesting queries", which consisted of one command:
fetch api_cursor0000000000000003
Further analysis revealed that these are queries from LinkedServer. The question immediately arose: “Is it really a request of this type select * from RemoteServer.RemoteDatabase.dbo.RemoteTable where FieldId = Value generates a request (fetch api_cursor0000000000000003) to RemoteServer?” It turns out, yes, and even when LinkedServer is MS SQL.
For a more illustrative example, create the table “Test” (the table creation code is available in the appendix to the article) on server “A”, and on server “B” we will execute the request:
select * from dev2.test_db.dbo.test
where dev2 is our server “A".
At the first execution of such a request, we will have a similar log in the profiler on server A:
Part of the log on server A
And now we’ll execute the requests by ID:
select * from dev2.test_db.dbo.test where ID = 3
Profiler log for the second request
As you can see in the screenshot, the request plan was added to the cache. If you run this request a second time, then a little better.
profiler log after restart
As we can see, the data is already taken from the cache.
When conditions change, we get a similar sample - the first sample for a given Id . But the bottom line is that with large numbers of different requests, the cache is not enough. And sql starts to make a bunch of queries to the table, which leads to "brakes". You ask: “What about the indices?” There are indexes, but even queries with a Primary Key (PK) condition caused this problem.
What does Google say about this? And a lot of things, but no sense:
- That requests must be executed from a user who belongs to one of the following roles: sysadmin, db_owner, db_ddladmin, so that statistics can be used;
- Incorrectly configured LinkedServer.
More sensible answers were found only in 3 articles:
- Exposing API Server Cursors
- Top 3 Performance Killers For Linked Server Queries
- Push and Pull technologies when working with linked servers in Microsoft SQL Server
As far as I understand, you cannot configure LinkedServer so that Pull technology is always used to receive data from LinkedServer. It all depends on where you are processing the request.
Time was running out, and the only solution that could save us was to rewrite some of the queries in dynamic sql. Those. execute requests on the server on which the data is stored.
There are several ways to work with data on LinkedServer:
- In the request, directly indicate the data source - the remote server. This implementation has several disadvantages:
- low productivity;
- returns a large amount of data.
select * from RemoteServer.RemoteDatabase.dbo.RemoteTable where Id = @Id
- Use OPENQUERY. Not suitable for a number of reasons:
- cannot specify the name of the remote server as a parameter;
- pass parameters to the request;
- There are also problems that were described in Dynamic T-SQL and how it can be useful.
select * from OPENQUERY(RemoteServer, 'select * from RemoteDatabase.dbo.RemoteTable').
The links provide sample logs for the following queries. These requests will be executed on server “B”, and the logs from server “A”:select * from OPENQUERY(dev2, 'select * from test_db.dbo.test') where id = 26
select * from OPENQUERY(dev2, 'select * from test_db.dbo.test where ID = 26')
- Run the request on the remote server. Similar to OPENQUERY:
- you cannot specify the server name as a parameter, since the name is specified at the stage of compilation of the procedure;
- There are also problems that were described in Dynamic T-SQL and how it can be useful.
exec ('select * from RemoteDatabase.dbo.RemoteTable') at RemoteServer
The links provide sample logs for the following queries:exec ('select * from test_db.dbo.test') at dev2
exec ('select * from test_db.dbo.test where Id = 30') at dev2
- exec at server / On the client side with the specified Id.trc
- exec at server / On the server side with the specified Id.trc
- It is still possible to execute the request on the remote server by running sp_executesql.
DECLARE @C_SP_CMD nvarchar(50) = QUOTENAME(@RemoteServer) + N'.'+@RemoteDatabase +N'.sys.sp_executesql' DECLARE @C_SQL_CMD nvarchar(4000) = 'select * from dbo.RemoteTable' EXEC @C_SP_CMD @C_SQL_CMD
Examples of query execution logs using sp_executesql are available via the links:- sp_executesql / Full fetch on client.trc
- sp_executesql / Full Server-side fetch .trc
- sp_executesql / Id selection on the client.trc
- sp_executesql / Id selection on server.trc
The fourth method was used to solve the problem.
Below are some graphs of incoming and outgoing traffic on the server where the main queue base is located before and after using sp_executesql. The size of the database is 200-300Mb.
incoming and outgoing traffic a few days on the server, before using sp_executesql

incoming and outgoing traffic, after the start of using sp_executesql

Outgoing peaks are copying backup to NFS.
The conclusion suggests itself: initially, the driver from MS for working with "MS sql linked server" cannot itself execute requests on the data source server. Therefore, colleagues, let's try to execute them at the data source, in order to solve at least part of the performance issues.
Files for the article.