{"id":210,"date":"2026-03-21T08:55:20","date_gmt":"2026-03-21T12:55:20","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=210"},"modified":"2026-03-21T09:38:31","modified_gmt":"2026-03-21T13:38:31","slug":"entity-framework-core-a-escala-con-millones-de-registros","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/03\/entity-framework-core-a-escala-con-millones-de-registros\/","title":{"rendered":"Entity Framework Core a escala: por qu\u00e9 tu app se cae a pedazos con millones de registros (y c\u00f3mo no dejar que pase)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Cuando est\u00e1s trabajando en proyectos peque\u00f1os o medianos con <strong><a href=\"https:\/\/en.wikipedia.org\/wiki\/Entity_Framework\">Entity Framework Core<\/a><\/strong>, todo fluye de lujo: consultas expresivas, c\u00f3digo limpio, productividad por las nubes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Y de repente\u2026 llegan los millones de registros.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No es que los datos crezcan poco a poco y te d\u00e9 tiempo a reaccionar. No. Un d\u00eda est\u00e1s c\u00f3modo con 50 000 filas y al siguiente tu aplicaci\u00f3n empieza a sufrir: consultas que antes iban en milisegundos ahora tardan segundos (o minutos), el <code>SaveChanges()<\/code> se convierte en una tortura, la memoria se dispara y aparecen timeouts por todos lados.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Este art\u00edculo es una gu\u00eda pr\u00e1ctica y directa para entender <strong>por qu\u00e9 pasa esto <\/strong>y, sobre todo, <strong>qu\u00e9 puedes hacer hoy mismo<\/strong> para que tu aplicaci\u00f3n siga respirando cuando los datos se ponen serios. Actualizado a 2026 con lo mejor que traen EF Core 8 y 9.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. \u00bfPor qu\u00e9 se rompe todo cuando hay muchos datos?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">EF Core no est\u00e1 mal hecho. Simplemente su dise\u00f1o por defecto est\u00e1 pensado para comodidad, no para escala extrema sin ajustes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">El gran culpable: el Change Tracker<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Cada vez que cargas una entidad, EF Core la mete en su <strong><a href=\"https:\/\/learn.microsoft.com\/en-us\/ef\/core\/change-tracking\/\">Change Tracker<\/a><\/strong>. Ese sistema:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>recuerda el estado original de cada objeto<\/li>\n\n\n\n<li>detecta qu\u00e9 cambi\u00f3<\/li>\n\n\n\n<li>genera los INSERT\/UPDATE\/DELETE correspondientes<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Suena genial\u2026 hasta que tienes cientos de miles o millones de entidades en memoria. El coste del Change Tracker crece casi linealmente (muchas operaciones internas son O(n)). Con 10 registros ni lo notas. Con 100 000 ya molesta. Con varios millones\u2026 es un cuello de botella brutal.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Peque\u00f1o vs grande: c\u00f3mo cambia el comportamiento<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Escenario<\/th><th>Qu\u00e9 pasa realmente<\/th><\/tr><tr><td>Proyecto peque\u00f1o<\/td><td>Todo r\u00e1pido, latencias bajas, GC casi invisible<\/td><\/tr><tr><td>Dataset grande<\/td><td>GC muy frecuente, CPU alta, consultas se ralentizan<\/td><\/tr><tr><td>Operaciones masivas<\/td><td>SaveChanges() puede pasar de segundos a minutos (o peor)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Se\u00f1ales de que algo va mal (tu radar personal)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>La memoria de la aplicaci\u00f3n sube sin parar (sobre todo dentro de bucles)<\/li>\n\n\n\n<li>El Garbage Collector est\u00e1 trabajando como loco<\/li>\n\n\n\n<li>CPU alta solo por operaciones de EF<\/li>\n\n\n\n<li>Ves consultas lentas en los logs de SQL<\/li>\n\n\n\n<li>Timeouts en operaciones que \u00abdeber\u00edan ser r\u00e1pidas\u00bb<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Si ves dos o m\u00e1s de estas\u2026 es hora de actuar.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Patrones de consulta que s\u00ed escalan<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Regla de oro para lecturas: <code>AsNoTracking()<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Si solo vas a leer y no vas a modificar nada, siempre pon:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>var data = context.Orders\n    .AsNoTracking()\n    .Where(x =&gt; x.Status == \"Completed\")\n    .ToList();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Esto le dice a EF: \u00abno me las metas en el Change Tracker\u00bb. Menos memoria, menos CPU, mejor rendimiento. Es de las cosas m\u00e1s baratas y efectivas que puedes hacer.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Proyecta a DTOs en vez de traer entidades completas<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No traigas el objeto gordo si solo necesitas tres campos:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>var orders = context.Orders\n    .Select(o =&gt; new OrderDto\n    {\n        Id = o.Id,\n        Total = o.Total,\n        Date = o.OrderDate\n    })\n    .ToList();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Menos datos por la red, menos objetos en memoria, menos trabajo para el Change Tracker.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Paginaci\u00f3n: olv\u00eddate del offset en tablas grandes<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Esto <strong>NO<\/strong> escala bien:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.Skip(100_000).Take(50)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A partir de cierto punto, el OFFSET grande obliga a escanear todo lo anterior &#8211;&gt; lent\u00edsimo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mejor: keyset pagination (o seek method)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.Where(x =&gt; x.Id &gt; lastId)\n.OrderBy(x =&gt; x.Id)\n.Take(50)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Aprovecha \u00edndices, es predecible y escala linealmente.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Nunca traigas TODO a memoria (por favor)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">El cl\u00e1sico error que he visto mil veces:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>var all = context.Logs.ToList();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Con 5 millones de filas &#8211;> adi\u00f3s memoria, hola OutOfMemoryException o GC pausando todo durante segundos.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">La forma adulta: procesar por lotes<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>int batchSize = 1000;\nlong lastId = 0;\n\nwhile (true)\n{\n    var batch = context.Logs\n        .Where(x =&gt; x.Id &gt; lastId)\n        .OrderBy(x =&gt; x.Id)\n        .Take(batchSize)\n        .AsNoTracking()\n        .ToList();\n\n    if (!batch.Any()) break;\n\n    \/\/ Procesar el batch (p.ej. enviar a un queue, calcular algo, etc.)\n    lastId = batch.Max(x =&gt; x.Id);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Este patr\u00f3n te salva la vida cuando tienes que tocar millones de filas.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">C\u00f3mo cazar consultas problem\u00e1ticas<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Activa logging de EF (con cuidado en producci\u00f3n)<\/li>\n\n\n\n<li>Usa SQL Profiler, pgAdmin, Azure Data Studio, etc.<\/li>\n\n\n\n<li>MiniProfiler o Application Insights en la app<\/li>\n\n\n\n<li>Busca: Includes enormes, patrones N+1, SELECT * innecesarios<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">4. Operaciones masivas (bulk): aqu\u00ed es donde m\u00e1s se sufre<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Lo que <strong>NO<\/strong> hacer<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>context.AddRange(muchasEntidades);\ncontext.SaveChanges();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">EF genera miles de INSERT individuales + tracking completo. Mal\u00edsimo.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Un poco mejor: batches con <code>SaveChanges<\/code><\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>int batchSize = 1000;\n\nforeach (var chunk in entities.Chunk(batchSize))\n{\n    context.AddRange(chunk);\n    context.SaveChanges();\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Mejor que nada, pero sigue usando Change Tracker y transacciones por batch.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Lo moderno (EF Core 7+ , mejorado en 8 y 9): ExecuteUpdate \/ ExecuteDelete<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Marcar como procesados miles o millones de filas\nawait context.Logs\n    .Where(l =&gt; l.Created &lt; thresholdDate &amp;&amp; !l.Processed)\n    .ExecuteUpdateAsync(setters =&gt; setters\n        .SetProperty(l =&gt; l.Processed, true));\n\n\/\/ Borrar \u00f3rdenes antiguas\nawait context.OldOrders\n    .Where(o =&gt; o.OrderDate &lt; twoYearsAgo)\n    .ExecuteDeleteAsync();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Una sola sentencia SQL, sin cargar nada en memoria, sin Change Tracker. Brutal diferencia.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Limitaci\u00f3n<\/strong>: solo puedes hacer lo que SQL entiende directamente. Si necesitas l\u00f3gica compleja por fila &#8211;&gt; toca batch + librer\u00eda bulk.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Librer\u00edas bulk para los casos duros<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">EFCore.BulkExtensions, Entity Framework Extensions, etc. dan:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>BulkInsert<\/li>\n\n\n\n<li>BulkUpdate<\/li>\n\n\n\n<li>BulkDelete<\/li>\n\n\n\n<li>BulkMerge \/ Upsert<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Siguen siendo la opci\u00f3n rey para inserts masivos o escenarios muy espec\u00edficos.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Dise\u00f1o del modelo y del esquema: donde se gana (o se pierde) mucho rendimiento<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">\u00cdndices: no es opcional, es obligatorio<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Si vas a filtrar, ordenar o unir por una columna con frecuencia, <strong>ponle \u00edndice<\/strong>. Punto.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Clave primaria &#8211;&gt; ya viene indexada (bien)<\/li>\n\n\n\n<li>Columnas de WHERE frecuentes (Status, Created, UserId, etc.) &#8211;&gt; \u00edndice simple<\/li>\n\n\n\n<li>Combinaciones comunes (WHERE + ORDER BY) &#8211;&gt; \u00edndice compuesto<\/li>\n\n\n\n<li>Columnas de JOIN &#8211;&gt; \u00edndice en la FK<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Sin \u00edndices adecuados, EF Core puede generar consultas decentes\u2026 pero la base de datos las ejecuta como escaneo completo. Y con millones de filas, eso es muerte.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Revisa tambi\u00e9n que las estad\u00edsticas de la base est\u00e9n actualizadas (en SQL Server: <code>UPDATE STATISTICS<\/code>, en Postgres suele ser autom\u00e1tico pero vale la pena chequear).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Separa lo que pesa<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Logs, auditor\u00eda, historial de cambios, eventos\u2026 <strong>no los metas en la misma tabla que las entidades principales<\/strong> si puedes evitarlo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Opciones reales:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Tablas separadas (Logs, AuditLog, OrderHistory\u2026)<\/li>\n\n\n\n<li>Tablas particionadas (por fecha, por tenant\u2026)<\/li>\n\n\n\n<li>Bases de datos separadas para escritura vs anal\u00edtica (m\u00e1s adelante tocamos CQRS)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Esto evita que una consulta pesada de reporting arrastre a toda tu aplicaci\u00f3n transaccional.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Denormalizaci\u00f3n controlada (s\u00ed, a veces hay que hacerlo)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Para reporting cr\u00edtico o consultas muy frecuentes, duplicar datos puede ser la opci\u00f3n m\u00e1s barata.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ejemplos comunes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Campo <code>TotalConImpuestos<\/code> calculado y guardado<\/li>\n\n\n\n<li>Tabla de \u00abOrderSummary\u00bb con datos precalculados<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Desde EF Core 7+ puedes usar columnas JSON de forma nativa (muy \u00fatil en Postgres con jsonb, en SQL Server con NVARCHAR(MAX) + chequeo JSON):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>modelBuilder.Entity&lt;Order&gt;()\n    .Property(o =&gt; o.Details)\n    .HasColumnType(\"jsonb\");  \/\/ Postgres\n    \/\/ o en SQL Server: .HasColumnType(\"nvarchar(max)\");<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Esto te permite guardar estructuras complejas sin crear 20 columnas extras.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Separaci\u00f3n ligera de lecturas y escrituras (CQRS \u00ablight\u00bb)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">No necesitas arquitectura de microservicios ni Event Sourcing para beneficiarte de esto.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Idea simple:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Escritura<\/strong>: usa EF Core normal (con Change Tracker, validaciones, l\u00f3gica de negocio)<\/li>\n\n\n\n<li><strong>Lectura<\/strong>: usa lo m\u00e1s r\u00e1pido y liviano posible<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Opciones para lecturas:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>SQL crudo con <code>FromSqlRaw()<\/code> o <code>FromSqlInterpolated()<\/code><\/li>\n\n\n\n<li>Dapper (muy liviano y r\u00e1pido)<\/li>\n\n\n\n<li>Vistas o materialized views en la base de datos<\/li>\n\n\n\n<li>Segundo DbContext optimizado solo para lectura<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Ejemplo de separaci\u00f3n de contextos:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ WriteDbContext --> Change Tracking activado, con todas las entidades\npublic class WriteDbContext : DbContext { ... }\n\n\/\/ ReadDbContext --> AsNoTracking por defecto en el constructor\npublic class ReadDbContext : DbContext\n{\n    public ReadDbContext(DbContextOptions options) : base(options)\n    {\n        ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En tu DI:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services.AddDbContext&lt;WriteDbContext&gt;(...);\nservices.AddDbContext&lt;ReadDbContext&gt;(...);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Y usas el que corresponda seg\u00fan el caso.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Bonus muy \u00fatil: DbContext Pooling para APIs de alto tr\u00e1fico<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">En aplicaciones con muchas peticiones concurrentes (Web API, Blazor Server, etc.):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>builder.Services.AddDbContextPool&lt;MyDbContext&gt;(options =&gt;\n    options.UseSqlServer(connectionString));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Reduce la creaci\u00f3n\/destrucci\u00f3n constante de DbContexts &#8211;&gt; menos presi\u00f3n en GC, mejor throughput bajo carga alta.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Caso pr\u00e1ctico real: reprocesar millones de registros<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">El enfoque que todos hacemos la primera vez (y que nunca m\u00e1s deber\u00edamos)<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>var logs = context.Logs.ToList();\n\nforeach (var log in logs)\n{\n    log.Processed = true;\n}\n\ncontext.SaveChanges();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Resultado con 3\u20135 millones de filas:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Memoria &gt; 3\u20135 GB<\/li>\n\n\n\n<li>Tiempo: 30 min a varias horas<\/li>\n\n\n\n<li>Posible timeout, OutOfMemory, base de datos bloqueada<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Enfoque realista y efectivo (2026)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">1. Si la actualizaci\u00f3n es <strong>uniforme<\/strong> (mismo cambio para todos los que cumplen condici\u00f3n) &#8211;> <code>ExecuteUpdateAsync<\/code><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>await context.Logs\n    .Where(l =&gt; l.Created &lt; threshold &amp;&amp; !l.Processed)\n    .ExecuteUpdateAsync(s =&gt; s.SetProperty(l =&gt; l.Processed, true));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tiempo t\u00edpico: segundos a pocos minutos.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">2. Si necesitas l\u00f3gica por registro &#8211;&gt; lectura por lotes + procesamiento + escritura optimizada<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>AsNoTracking()<\/code> + keyset pagination<\/li>\n\n\n\n<li>Procesar batch<\/li>\n\n\n\n<li>Guardar con <code>ExecuteUpdateAsync<\/code> cuando sea posible, o con librer\u00eda bulk<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">3. Para inserts o upserts masivos &#8211;&gt; EFCore.BulkExtensions o similar<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Resultados t\u00edpicos que ves en la vida real (aprox.)<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Estrategia<\/th><th>Tiempo (millones de filas)<\/th><th>Memoria pico<\/th><\/tr><tr><td>Cargar todo + SaveChanges<\/td><td>30\u2013120 minutos<\/td><td>&gt;2\u20135 GB<\/td><\/tr><tr><td>Batches de 1000 con SaveChanges<\/td><td>5\u201315 minutos<\/td><td>300\u2013800 MB<\/td><\/tr><tr><td>ExecuteUpdateAsync \/ ExecuteDelete<\/td><td>30 segundos \u2013 3 minutos<\/td><td>&lt;200 MB<\/td><\/tr><tr><td>Librer\u00eda bulk (insert\/update)<\/td><td>10 segundos \u2013 2 minutos<\/td><td>&lt;150 MB<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">La diferencia no es \u00abun poquito mejor\u00bb. Es estructural.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">8. Anti-patrones que veo todo el tiempo (ev\u00edtalos)<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Llamar <code>.ToList()<\/code> o <code>.ToArray()<\/code> \u00abpor si acaso\u00bb<\/li>\n\n\n\n<li>Abusar de <code>.Include()<\/code> y traer medio grafo de entidades<\/li>\n\n\n\n<li>Usar EF Core para reporting anal\u00edtico pesado (dashboards, BI\u2026)<\/li>\n\n\n\n<li>Transacciones gigantes que abarcan miles de cambios<\/li>\n\n\n\n<li>No poner \u00edndices en columnas filtradas<\/li>\n\n\n\n<li>Mezclar lecturas pesadas y escrituras en el mismo contexto<\/li>\n\n\n\n<li>No usar DbContext Pooling en APIs con alta concurrencia<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">9. Checklist r\u00e1pido antes de que tu app escale (o cuando ya est\u00e9 sufriendo)<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u00bfEst\u00e1s usando <code>AsNoTracking()<\/code> en todas las lecturas puras?<\/li>\n\n\n\n<li>\u00bfProyectas a DTOs o tipos an\u00f3nimos cuando no necesitas modificar?<\/li>\n\n\n\n<li>\u00bfUsas keyset pagination en vez de offset para p\u00e1ginas profundas?<\/li>\n\n\n\n<li>\u00bfProcesas grandes vol\u00famenes por lotes?<\/li>\n\n\n\n<li>\u00bfEst\u00e1s aprovechando <a href=\"https:\/\/learn.microsoft.com\/en-us\/ef\/core\/saving\/execute-insert-update-delete\"><code>ExecuteUpdateAsync<\/code> y <code>ExecuteDeleteAsync<\/code><\/a>?<\/li>\n\n\n\n<li>\u00bfTienes \u00edndices en todas las columnas de WHERE\/ORDER BY\/JOIN frecuentes?<\/li>\n\n\n\n<li>\u00bfLas estad\u00edsticas de la base est\u00e1n actualizadas?<\/li>\n\n\n\n<li>\u00bfUsas librer\u00edas bulk para inserts masivos o merges?<\/li>\n\n\n\n<li>\u00bfHas considerado separar lecturas y escrituras (al menos con dos contextos)?<\/li>\n\n\n\n<li>\u00bfMonitoreas logs SQL + m\u00e9tricas de memoria\/CPU en producci\u00f3n?<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Ajusta EF Core a medida del tama\u00f1o de tu App<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Entity Framework Core es una herramienta incre\u00edble. En proyectos peque\u00f1os o medianos te hace sentir como superh\u00e9roe: escribes c\u00f3digo expresivo, cambias de base de datos en minutos, y todo \u00absimplemente funciona\u00bb.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pero cuando los datos pasan de \u00abmiles\u00bb a \u00abmillones\u00bb, EF Core no se rompe\u2026 <strong>se comporta exactamente como est\u00e1 dise\u00f1ado<\/strong>. El problema casi nunca es la librer\u00eda. Es que seguimos us\u00e1ndola con los mismos patrones que us\u00e1bamos cuando ten\u00edamos 10 000 filas.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La buena noticia: con ajustes relativamente simples (AsNoTracking, proyecciones, paginaci\u00f3n keyset, batches, ExecuteUpdate\/Delete, \u00edndices decentes y, cuando haga falta, una librer\u00eda bulk) puedes multiplicar el rendimiento por 10\u00d7, 50\u00d7 o incluso 100\u00d7 sin tener que abandonar EF Core.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La clave est\u00e1 en <strong>saber d\u00f3nde EF Core te da valor<\/strong> (l\u00f3gica de negocio, transacciones, consistencia) y <strong>d\u00f3nde te est\u00e1 costando caro<\/strong> (lecturas masivas, reporting, operaciones bulk). En esos segundos casos, no dudes en usar herramientas m\u00e1s livianas o SQL directo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As\u00ed que la pr\u00f3xima vez que alguien te diga \u00abes que EF Core no escala\u00bb, preg\u00fantale cu\u00e1ntos registros tiene y c\u00f3mo est\u00e1 escribiendo las consultas. Porque la mayor\u00eda de las veces no es que EF Core no escale\u2026 es que lo estamos usando como si todav\u00eda tuvi\u00e9ramos una base de datos de pruebas con 500 filas.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u00bfTe ha pasado alguna de estas situaciones? \u00bfQu\u00e9 truco te ha salvado el pellejo con millones de registros? Cu\u00e9ntame en los comentarios, que siempre se aprende algo nuevo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Sigamos codificando.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cuando est\u00e1s trabajando en proyectos peque\u00f1os o medianos con Entity Framework Core, todo fluye de lujo: consultas expresivas, c\u00f3digo limpio, productividad por las nubes. Y de repente\u2026 llegan los millones de registros. No es que los datos crezcan poco a poco y te d\u00e9 tiempo a reaccionar. No. Un d\u00eda est\u00e1s c\u00f3modo con 50 000 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[18],"tags":[11,20],"class_list":["post-210","post","type-post","status-publish","format-standard","hentry","category-guia","tag-c","tag-sql"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/210","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=210"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/210\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=210"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=210"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=210"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}