I’ve been there. You’re staring at a dashboard in your multi-tenant app, and it’s crawling. The culprit is a query filtering across tens of thousands of records. Your first instinct? “LINQ is too slow. I’ll just write the raw SQL myself.”

It’s a tempting thought. But dropping down to raw SQL isn’t a simple performance switch. Get it wrong, and you could introduce a security hole or, ironically, make performance even worse. This exact scenario burned me once in production, and the fix wasn’t what I expected.

Let’s break down when to stick with LINQ and when (and how) to safely write your own SQL in EF Core.

First, The Big One: Security

If you take one thing away from this post, let it be this: never concatenate user input into a SQL string. SQL injection isn’t a textbook problem; it’s a real threat that I’ve seen take down production systems.

It’s easy to make this mistake, especially with C#’s slick string interpolation. This looks safe, right?

// DANGER: NEVER DO THIS.
var userId = Request.Query["userId"]; // e.g., "123"
var sql = $"SELECT * FROM Users WHERE Id = {userId}";
var users = await context.Users.FromSqlRaw(sql).ToListAsync();

This code is a wide-open door. An attacker can send 123; DROP TABLE Users; -- as the userId, and boom, your user table is gone.

The critical gotcha is that standard C# string interpolation ($"...") does not protect you. It just builds a string. The magic happens when you use EF Core’s specific methods.

Here’s the right way to do it:

// CORRECT: EF Core handles parameterization for you.
var userId = Request.Query["userId"];
var users = await context.Users
    .FromSqlInterpolated($"SELECT * FROM Users WHERE Id = {userId}")
    .ToListAsync();

Notice the method changed from FromSqlRaw to FromSqlInterpolated. Behind the scenes, EF Core isn’t just smashing strings together. It generates a parameterized query like SELECT * FROM Users WHERE Id = @p0 and sends the userId value separately. This completely neutralizes SQL injection attacks.

Okay, But Is Raw SQL Faster? I Ran the Numbers.

So, assuming we’re using safe, parameterized queries, is raw SQL still a performance win? I set up a BenchmarkDotNet test to find out. The test runs 1,000 individual lookups against a SQL Server database with 10,000 records.

The results were… weird.

MethodMeanNotes
BulkLinqApproach20.58 msWinner: One trip to the DB
ComplexAnalyticalQuery19.77 msRaw SQL for complex logic shines
LinqApproach387.21 msBaseline for N+1 lookups
ParameterizedRawSql421.91 msSlightly slower than LINQ!
StringConcatenationRawSql1,043.84 msLoser: 2.7x slower, and insecure

Here’s what this data really tells us:

  1. String Concatenation is Genuinely Awful: At over 1 second, it was by far the slowest. Why? SQL Server has to compile a new query plan for every single unique query string. This pollutes the plan cache and adds massive overhead.
  2. LINQ is Really, Really Good: For simple lookups, modern EF Core’s LINQ-to-SQL translation is highly optimized. It was actually slightly faster than the parameterized raw SQL equivalent. The overhead difference is negligible.
  3. The Real Enemy is Round Trips: Look at the BulkLinqApproach. It fetched all 1,000 records in a single query and blew everything else away at just 20ms. The N+1 problem (one query to get the list, then N queries to get details) is almost always a bigger performance killer than how the query is written.

So, the whole “is LINQ slower than raw SQL?” debate is often asking the wrong question. The right question is, “Am I making too many trips to the database?”

When I Actually Drop Down to Raw SQL

After seeing those numbers, my default is to stick with LINQ. It’s type-safe, easier to compose, and its performance is excellent. But there are a few specific cases where I’ll reach for FromSqlInterpolated or ExecuteSqlInterpolatedAsync.

1. For Complex Analytics LINQ Can’t Handle

Sometimes you need Common Table Expressions (CTEs), window functions (ROW_NUMBER()), or other advanced SQL features that LINQ just can’t generate cleanly.

// Good use case: Get monthly sales stats for a tenant
var monthlyStats = await context.Database
    .SqlQuery<MonthlyStatsResult>($@"
        WITH MonthlySales AS (
            SELECT 
                EOMONTH(OrderDate) as MonthEnd,
                SUM(TotalAmount) as Revenue
            FROM Orders 
            WHERE CustomerId = {tenantId}
            GROUP BY EOMONTH(OrderDate)
        )
        SELECT MonthEnd, Revenue FROM MonthlySales 
        WHERE Revenue > {minRevenue}
        ORDER BY MonthEnd DESC")
    .ToListAsync();

Trying to write this in LINQ would be a painful, unreadable mess, if it’s even possible. Raw SQL is the clear winner here.

2. For Bulk Updates and Deletes

If you need to update thousands of rows, loading them all into EF Core’s change tracker is a huge waste of memory and CPU. A single UPDATE or DELETE statement is far more efficient.

// Perfect for bulk operations
var multiplier = 1.10m; // 10% price increase
var categoryId = 5;

var affectedRows = await context.Database
    .ExecuteSqlInterpolatedAsync($@"
        UPDATE Products 
        SET Price = Price * {multiplier}
        WHERE CategoryId = {categoryId}");

This is a fire-and-forget operation that runs entirely on the database server. It’s fast and efficient.

3. Calling Stored Procedures

If your database logic lives in stored procedures, ExecuteSqlInterpolatedAsync is the direct path to calling them.

var result = await context.Database
    .ExecuteSqlInterpolatedAsync($@"
        EXEC GenerateMonthlyReport 
            @TenantId = {tenantId}, 
            @StartDate = {startDate}");

The Final Takeaway: My Playbook

So, what’s the verdict? Here’s the simple playbook I follow in my own projects.

  • When to use LINQ (90% of the time): For all standard create, read, update, and delete operations. It’s type-safe, maintainable, and the performance is fantastic. Before you blame LINQ for being slow, make sure you aren’t creating an N+1 query.

  • When to use Parameterized Raw SQL (FromSqlInterpolated): When you need to run a complex SELECT query that LINQ can’t express well (like queries with CTEs or window functions). It gives you the full power of SQL without sacrificing security.

  • When to use ExecuteSqlInterpolated: For bulk UPDATE, DELETE, or INSERT operations that would be inefficient to handle through the change tracker, and for calling stored procedures.

  • When to use String Concatenated SQL: Never. Just don’t. It’s insecure and, as the benchmarks show, often slower.

The real performance wins don’t come from micro-optimizing LINQ vs. raw SQL. They come from designing your queries to reduce round trips and letting the database do what it does best: work with large sets of data.

References

FAQ

What is a parameterized query in EF Core?

A parameterized query uses placeholders for input values, which EF Core binds at execution time to prevent SQL injection and improve plan caching.

Is raw SQL in EF Core safe to use?

Raw SQL is safe if you use EF Core’s parameterization methods like FromSqlInterpolated or ExecuteSqlInterpolated and avoid string concatenation of user inputs.

Why should I avoid string concatenation in SQL queries?

String concatenation can lead to SQL injection vulnerabilities and poor query plan caching, causing security risks and performance degradation.

When should I use raw SQL instead of LINQ in EF Core?

Use raw SQL for complex queries not expressible in LINQ, bulk operations, or calling stored procedures where performance or expressiveness demands it.

How can I measure the performance of my EF Core queries?

Enable EF Core logging to see generated SQL and execution times, and use SQL Server’s Query Store and DMVs to analyze query plan caching and performance.

About the Author

@CodeCrafter is a software engineer who builds real-world systems , from resilient ASP.NET Core backends to clean, maintainable Angular frontend. With 11+ years in production development, he shares what actually works when shipping software that has to last.