Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

kokoshneta's avatar

Laravel PDO slows down exponentially; native PDO does not – why?

I’ve found lots of threads, both here and on StackOverflow, of people complaining about Eloquent being slow for larger queries, but everywhere I just see suggestions to check indexes, checking the SQL query for efficiency, using raw statements instead of Eloquent/query builder to avoid creating collections of model instances, etc. – none of which answers the question on its own.

To be perfectly clear: I am aware that building a collection of model instances takes longer than just returning an array. I am aware that chunking or paginating result sets reduces memory usage and CPU time. I am aware that Eloquent inherently has overhead that makes it less efficient than native PDO queries. These are all factors that play in, but they are by no means enough to explain the huge discrepancy between native PDO and Laravel’s PDO.

Setup

  • A remote VPS server running Windows Server 2019, managed through Plesk
  • An SQL Server (SQLEXPRESS 2019) database on the remote server
  • My local dev machine, two Macs running macOS 10.15.7 and 12.2.1

On the dev machines, I have PHP, nginx, etc. set up through Homebrew, and Laravel set up using Valet. I don’t have Laravel deployed to the remote server yet.

The problem

Whenever I retrieve data from the remote database in my local Laravel app, queries are fine and snappy as long as they don’t return too many rows; but as the number of rows returned grows, query times grow exponentially – which they don’t normally, outside of Laravel.

For example, I have a product table with about 2,500 rows and 44 columns (which is way too much, but I didn't design it). To fetch all current products in a sensible order, I do this:

// Eloquent query
$products = Product::where('active', 1)->orderBy('year', 'desc')->orderBy('available_date', 'desc')->orderBy('name', 'asc')->get();

// Raw SQL query
DB::getPdo()->query("SELECT * FROM products WHERE active = 1 ORDER BY year DESC, available_date DESC, name ASC")->fetchAll(\PDO::FETCH_ASSOC);

That query returns just over 2,000 rows.

The columns used in the where and order clauses are all indexed; other columns are not. Non-indexed columns include an nvarchar(3000) column and an ntext column – so yes, indexes and sheer data volume are of course an issue, but not the whole story.

Timing queries from different sources

  • When I run the query from within Laravel as an Eloquent query, it takes about 20 seconds. And that’s just the query. Debugbar gives almost identical running times for the two versions of the query, and almost identical total page load times as well, so it’s not even generating the 2,000 model objects that makes it slow – associative array vs collection of objects adds less than a second to the total load time.
  • If I run the query from Azure Data Studio (ADS) on my Mac, the Messages section says the query takes about 0.4 seconds, about 50 times faster than Laravel. Displaying the results in the Results grid takes perhaps a second or so after that.
  • If I run the query using a plain PDO connection in a ‘plain’ PHP file on the remote server (where there’s no Laravel installed at all), the query takes about 0.2 seconds, so it’s about 100 times faster.

If I only select the indexed columns and leave everything else out, things look rather different:

  • Laravel (Eloquent or ‘plain’) fetches the rows in about 400ms
  • ADS does it in about 50-60ms
  • Plain PDO locally from the remote server does it in about 17ms

Given that at least some of the time for Laravel and ADS is used on just sending and receiving the request from the remote server, the actual differences are even smaller than this. So ADS still takes about twice as long as the local request, but Laravel now ‘only’ takes perhaps 20 times as long, not 100 times.

The same can be observed if I reduce the number of rows, but keep all the unindexed columns. Reducing to 500 / 50 rows (respectively):

  • Laravel does it in about 500ms / 5 seconds
  • ADS does it in 50-60ms (same as above) / 200ms
  • Local request does it in 28ms / about 80ms

Compared to the local requests, the Laravel requests go from about 8 times slower at 50 rows to 18 times slower at 500 rows, and then of course 100 times slower at 2,000 rows.

What can be the cause of this?

It’s understandable if Laravel, being a complete ORM, is slower than plain PHP, but it should not be 100 times slower – and certainly, it should not be exponentially slower for any reason I can fathom. I would expect it to be slower at a reasonably constant rate.

What are some possible pitfalls and reasons why running a query through Laravel can be that much slower, in particular exponentially so?

(The one test really missing is of course running a plain PDO query from my dev machine – but unfortunately I simply cannot get the connection to work. I can connect using Laravel’s DBAL-based drivers, but I can’t get a simple PDO connection up and running from my localhost.)

0 likes
28 replies
sr57's avatar

Creating a collection means under the hook a foreach for all records from the result of the query.

The pdo result just give the result "as is".

With this you see your exponential factor.

In real life, you use pagination, that uses a small number of record and make Laravel Ok.

kokoshneta's avatar

@sr57 This is precisely why I specifically included a query that doesn’t create a collection, or indeed objects, and why I started off the question by stating that the difference is NOT just one of Eloquent creating objects and collections.

As I clearly state in the quesetion, fetching the data ‘as-is’, as a simple array, is almost as slow as creating a collection of objects; it’s a difference between 19 and 20 seconds loading time, perhaps. In the non-Laravel code on the remote server, I also have model-like classes and a collection class (not as advanced as Laravel’s, but of a similar basic structure); fetching the data into such a collection of class objects is a bit slower than fetching a simple array, but it is still many times faster than fetching a plain array through Laravel – the difference between 0.2 and 0.25 seconds.

Fetching 2,000 rows from a database is a trivial task that should not require pagination to be usable, not even with a model-based ORM, and certainly not without one.

kokoshneta's avatar

@sr57 I would invite you to reread the question. If you believe something is incorrect in it, please say what it is.

jlrdw's avatar

@kokoshneta I use eloquent where data is paginated, i.e., someone needs to see accounts receivable on screen one company at a time.

However if it is a regular detail report I then use regular pdo also. Usually a summary report is all that's needed but at times someone may want a complete detail report printed.

98% of the time especially with paginated results there is hardly a time difference, only when having to deal with a huge number of records. In such a case php pdo will be faster than eloquent.

But just my observations.

Edit:

Try with pagination or chunk on your queries.

kokoshneta's avatar

@Snapey You mean in the queries section? Or the timeline section?

When running the Eloquent query, the query output in the queries section is the same as the raw query in the code snippet in the question, with a timing of those 19–20 seconds or so. When running the raw query directly on the underlying PDO object, the query doesn’t show up in Debugbar at all (I timed those manually using microtime()).

kokoshneta's avatar

@jlrdw I do use pagination and chunking most of the time when loading many records. There are only a couple of places where I need to load more than 12 records maximum, but for two of those, pagination won’t work – I need all the records in one long list in a synchronous file.

The trouble I’m having now is that I can’t find any way to do that that during development doesn’t involve waiting 20 seconds on each page load, which is frustrating. Once deployed, I should be able to use native PDO objects to connect with no problems, but since I can’t get native PDO connections to work from my development machine, that’s not an option yet.

sr57's avatar

@kokoshneta

Sorry English is not my mother language so I have not your ease to write. So ask If don't understand just ask or reformulate to be sure you understand what I think.

Your OP is long, and there are facts (numbers with codes) and only texts. So I took your 2 main facts (1 eloquent query and one PDO query)

Your eloquent query returns a collection

I used Laravel for about 1 year and did theses types of tests at the beginning, and you are right Pdo is faster, than DB facade that is faster than Eloquent.

I never found such difference as you wrote, but numbers depend of many things and if you want to share your exact tests I can do the same on a postgresql db.

Yes , Fetching 2,000 rows from a database is a trivial task, but not a pagination of 2000 ... we are working with web server, not data management,.

PS I don't use Debuggar but my own debug.

kokoshneta's avatar

@sr57 Yes, the Eloquent query does return a collection.

But the Laravel PDO query (the second query in the code block, the one that begins with DB::getPdo()->query(…)) doesn’t. It uses Laravel’s underlying PDO object (that’s what I’m calling the ‘Laravel PDO’), and it returns the result as a simple array.

But it still takes almost as long as returning the collection. That’s what puzzles me most. I would have thought that this should be fast:

$pdo = DB::getPdo();
$products = $pdo->query("SELECT [etc.]")->fetchAll(\PDO::FETCH_ASSOC);

But it’s not. It’s very slow.

On the other hand, if I do this:

$pdo = new PDO("sqlsrv:Server=XYZ;Database=XYZ", $db_user, $db_pass);
$products = $pdo->query("SELECT [etc.]")->fetchAll(\PDO::FETCH_ASSOC);

Then it’s very fast. But unfortunately, I can only do that on the remote server, because I can’t get it to connect from my Mac.

sr57's avatar

@kokoshneta

I don't write this time reread my answer, but I write the 2 important points of it

Point 1

very slow ... very fast

text ... (note 1)

Point 2

I never found such difference as you wrote, but numbers depend of many things and if you want to share your exact tests I can do the same on a postgresql db.

PS :

(note 1) With a 2000 rows table I found a difference of ~20% I don't know what do you see with debuggar but for your information, getPDO uses some additonal classes, like

Illuminate\Support\Facades\DB

Illuminate\Support\ConfigurationUrlParser

Illuminate\Database\Grammar

(note 2) The only follow-up (and not before next week) I will give to this discussion is, if you wish, the realization of an IDENTICAL benchmark

kokoshneta's avatar

@sr57 A 20% difference would be exactly the sort of thing I would expect (and be fine with). That’s non-exponential and precisely as it should be. Here is a Debugbar screenshot for the query limited to 500 rows, using the Laravel PDO object (not Eloquent).

I did suspect that in Laravel, even using DB::getPdo(), what I’m getting back is not really a standard PDO object, but a ‘special’ one, even though get_class($pdo) just outputs PDO. But I haven’t been able to figure out what else might be special about it, or how it might differ from a plain PDO object.

And yes, I’m aware the benchmarks should be identical. They would be if I could just get plain PDO to connect to the server, but the closest I can get is “Login timeout expired”.

kokoshneta's avatar

@Sinnbeck I don’t have Xdebug installed, but that can be rectified. I’ve somehow managed to avoid dealing with Xdebug all these years, though, so it will probably take me a while to get up to speed on how to work it.

Sinnbeck's avatar

@kokoshneta You can also use dd() and just reinstall packages when you are done :) But personally I prefer xdebug as I can step through the code.

kokoshneta's avatar

@Sinnbeck Actually, that worked! I dumped the output of buildConnectString, and that gave me a connect string that actually allows me to connect using a native PDO object, huzzah! The problem was the one about a self-signed certificate, so I just needed to add the ‘trust certificate’ parameter to the connect string, then it worked.

So now I should be able to update my benchmarks to run from the same environment, at least!

Sinnbeck's avatar

@kokoshneta Awesome :) Hope you get closer to a solution. Btw I use sqlsrv as well, but dont have such problems (that I have noticed at least). If I am able to replicate it, I will let you know. I run laravel 8 btw.

Snapey's avatar

i meant does debugbar show a single query, what is that query, and what is the time taken next to that query

ive seen many projects load far too many records yet never be as slow as this

kokoshneta's avatar

@Snapey Yes, just one single query:

select * from [products] where [active] = 1 order by [year] desc, [available_date] desc, [name] asc

(With top 50(0) added for the limited tests and the individual column names instead of * for the column-limited test.)

The times shown in/next to that particular query are the ones I gave in the question, so 19–20 seconds for the unlimited query using Eloquent or Laravel’s PDO, etc.

The times match the microtime() testing as well (which I had to use where Debugbar isn’t available, so I added it inside Laravel for comparability as well). The last test I ran was for 500 rows using Laravel PDO: Debugbar gives 5.13 seconds, microtime gives 5.1261730194092 seconds (see Debugbar screenshot for that page load.

Snapey's avatar

@kokoshneta so next to that query in debugbar it says 20 seconds? Or the whole request cycle is 20 seconds?

Clearly something is wrong with your specific setup

kokoshneta's avatar

@Snapey Yes, if I leave out the TOP 500 bit, it says 20 or so seconds for that specific query. The whole request cycle is then something like half a second or a second longer.

Something must be wrong somehow – I just wish I knew what!

kokoshneta's avatar

Okay, so, with @sinnbeck’s help, I’ve now managed to connect to the server from my Mac using a native PDO connection.

Redoing the benchmark this way shows me that the problem is not, after all, between native PDO on the one hand and Eloquent/Laravel PDO on the other – but instead between running from my development machine and running from the remote server directly.

Selecting the ‘full’ query (all columns, no row limit) gives the following:

-- PLAIN PDO --
Time: 17.79839515686 s
Records: 2014

-- LARAVEL PDO --
Time: 17.119776964188 s
Records: 2014

A totally insignificant difference. So the problem is then that I’m connecting from a remote machine, or perhaps something related to the drivers installed on my Mac.

So I won’t worry too much about this now. I’ll optimise the query as much as I can, and then I’ll live with the slow load times until I deploy to the live server and see how it looks there.

kokoshneta's avatar

@Sinnbeck Nope, no VPN. It must be somehow related to row size and/or indexes, because only fetching the indexed columns takes the time down to about 300 ms. Unlike on the remote server, where the difference between fetching all columns and only fetching indexed columns is only about 200 ms.

Perhaps it’s just an inefficiency in the sqlsrv driver for Mac. I’ve seen claims that ODBC is faster, so I might try that.

kokoshneta's avatar
kokoshneta
OP
Best Answer
Level 27

Aha – a breakthrough! The claims that ODBC is faster actually led me on the right path here.

Searching, I found this old GitHub issue about PDO + sqlsrv being slow, with PDO + ODBC much faster.

This comment noted that latency seems to affect sqlsrv much more than other drivers, which makes sense in my case – there’s a lot of latency between my dev machine and the remote server on an entirely different continent, whereas locally executed queries of course have no latency at all. The repo actually has a wiki of recommendations for improving performance, which notes that setting multiple_active_result_sets to FALSE helps things – and boy did it!

Disabling multiple active result sets, the benchmark now looks like this:

-- PLAIN PDO --
Time: 1.1357409954071 s
Records: 2014

-- LARAVEL PDO --
Time: 1.1680629253387 s
Records: 2014

So that’s cut it down from 17+ seconds to just over 1 second! Still not stellar performance, but fully within the realm of the acceptable, considering the latency. Luckily, I don’t think I have anything that requires multiple active result sets anywhere.

1 like
Sinnbeck's avatar

@kokoshneta Nice find! Might need to apply some of that to my own app. It isn't exactly slow, but any optimization would be great :)

Sinnbeck's avatar

It seems to cut a about 1/4 of the response time. Not bad!

Please or to participate in this conversation.