Necesito decirte algo, y me tomó mucho tiempo admitirlo. Durante la mayor parte de mi carrera, pensé que escribir SQL crudo es un signo de fracaso. Significa que no puedes hacer que el ORM haga lo que querías. Significa que tus abstracciones se habían roto. Ingenieros superiores usaron ORMs. Ingenieros junior escribieron SQL. Yo estaba equivocado. No sólo estaba equivocado. Esa creencia era activamente dañina. Me llevó a escribir código que parecía limpio y eficiente, pero en realidad era un desastre de rendimiento. ¿Puede adivinar cuántas consultas SQL se tomaron para renderizar una sola página en una de nuestras aplicaciones? Y el número es 47.47 rondas de viajes a la base de datos y de vuelta para mostrar veinte filas de datos. El código que produjo era absolutamente limpio. Tipo seguro. Completamente probado. Con hermosas abstracciones. Y el código estaba destruyendo silenciosamente nuestros tiempos de respuesta.Y nadie había notado eso porque la abstracción había ocultado el crimen de nosotros. Una cosa que nadie pone en el material de marketing de ORM es que es muy bueno en ocultar lo que realmente está haciendo con la base de datos. Hablemos de lo que realmente tienes ORM trae mucho a la mesa. Y eso es parte del problema. Algunas de esas cosas son tan buenas que pueden hacerle daño sin que usted sepa. ORM trae la gestión de esquemas - migraciones de versiones, rollbacks y esquema difing. Y esa parte es fantástica. Además, trae una buena API para operaciones de objetos simples - 'findById', 'crear', 'actualizar' y 'eliminar'.Y esa parte también es fantástica.El código es limpio, el tipo de seguridad es real, y no hay costo de rendimiento significativo. Lo siguiente - mapear entre objetos y tablas. Y esto es una parte central de ORM. Y también, este es el lugar donde comienzan los problemas. Este mapeo es perfecto cuando funciona y crea un problema silencioso cuando no lo hace. ORM trae una ilusión de portabilidad de bases de datos. Y sí, en algún mundo ideal puedes cambiar de MySQL a PostgreSQL sin cambiar una línea de código. Pero en el mundo real, nunca lo haces. Usted elige una base de datos y se adhiere a ella. Y persiguiendo esa ilusión te hace evitar las características específicas de la base de datos que realmente harían que tu aplicación sea rápida. Cada abstracción hace compromisos.Si las entiendes, sobrevivirás.Si no, acabarás con cuarenta y siete (o incluso más) consultas para mostrar veinte líneas. El problema N+1 es un síntoma, no la enfermedad Casi todos los desarrolladores que han trabajado con ORM han oído hablar del problema de la consulta N + 1. la relación de carga lenta dentro de una loop, y esto desencadenará una consulta por iteración. El problema N+1 es sólo una parte visible del iceberg de los problemas ORM - hacen que sea demasiado fácil escribir código que parezca eficiente pero es un desastre bajo el capó. Mis 47 consultas no eran todos los patrones N + 1. Algunas eran búsquedas redundantes. En algunas de ellas, ORM estaba cargando entidades completas cuando sólo se necesitaban dos campos. Algunas de ellas eran consultas automáticas COUNT enviadas por la biblioteca de paginado. Y nada de esto es obvio a partir de la lectura del código de la aplicación! ¡Sólo se puede ver mirando el registro de la consulta. Y SQL crudo le da la experiencia opuesta. usted sabe exactamente qué consultas están siendo enviadas a la base de datos. Compruebe este SQL crudo. SELECT id, name, email FROM users WHERE account_id = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 20 OFFSET 0; Usted conoce la consulta.Y usted conoce las columnas exactas que se están recuperando.Puede pensar en los índices que necesitará para esta consulta.Y también, puede pegarlo directamente a su consola de bases de datos y ejecutar el comando EXPLAIN en él. Con el ORM, todo lo que sabes es la intención.La implementación real será más tarde en el tiempo de ejecución.Y las consultas reales pueden cambiar con el tiempo con los cambios de su modelo, y es posible que no tenga control sobre ellas. Donde los ORMs realmente no pueden seguirte Hay una clase de problemas en los que ORM no es sólo lento. Simplemente no puede manejarlos. Funciones de ventana Funciones de ventana - `RANK()`, `ROW_NUMBER()`, `LAG()`, 'LEAD()`, totales en marcha, medias móviles - son una de las características más poderosas de SQL moderno. SELECT user_id, amount, order_date, SUM(amount) OVER ( PARTITION BY user_id ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_total FROM orders WHERE account_id = ?; Esta consulta generará un total de ejecución para la cuenta en una única solicitud. es rápido, elegante y correcto. Si intenta implementar lo mismo en el constructor de consultas de ORM, no podrá hacerlo. Así que hay dos opciones para realizar ese cálculo.Usted o arrastra todas las filas a la memoria y la calcula en el nivel de la aplicación, o escribe SQL crudo.No hay tercera opción. Operaciones Bulk Imagina el escenario. Necesitas insertar 10.000 registros de una API externa y necesitas insertarlos o actualizarlos en tu base de datos. Se deben insertar nuevos registros y se debe actualizar los existentes si los datos entrantes son más recientes. El enfoque de ORM: foreach ($incomingRecords as $data) { $record = $this->repository->findOneBy(['external_id' => $data['id']]); if ($record === null) { $record = new SyncRecord(); $record->setExternalId($data['id']); } $record->setPayload($data['payload']); $record->setSyncedAt(new \DateTimeImmutable()); $this->em->persist($record); } $this->em->flush(); Para procesar 10k de registros tendremos que ejecutar 10.000 consultas seleccionadas y 10.000 consultas seleccionadas o actualizadas. El enfoque SQL: INSERT INTO sync_records (external_id, payload, synced_at) VALUES (?, ?, NOW()), (?, ?, NOW()), ... ON DUPLICATE KEY UPDATE payload = VALUES(payload), synced_at = VALUES(synced_at); Batchado en pedazos de 500-1,000 líneas. Tiempo total de ejecución: segundos. 'ON DUPLICATE KEY UPDATE' es una característica de MySQL. No tiene equivalente ORM. La abstracción simplemente no puede cubrir esto. Operaciones JSON Si está almacenando JSON en su base de datos, y después de que MySQL 8 y PostgreSQL lo hicieran bastante útil. ¿Filtrar por un camino JSON? Indexar en un valor de campo JSON? Usando 'JSON_TABLE' para convertir una matriz JSON en filas? ¿Estas son operaciones reales que las bases de datos manejan bien. También son operaciones que los ORM manejan mal, parcialmente, o no en absoluto. -- Find all products where attributes contain color = 'blue' SELECT * FROM products WHERE JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.color')) = 'blue'; -- This works because we created a functional index: -- CREATE INDEX idx_color ON products((JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.color')))); En el momento en que necesites algo así, estás escribiendo SQL crudo de todos modos.Técnicamente, puedes convertir el campo de json interno en un campo virtual en la tabla, pero es más como un truco. Debugging: el impuesto oculto Aquí está un escenario que probablemente has vivido. Probablemente se acuerde de una consulta lenta en la producción, no muy lenta, pero lo suficientemente lenta como para que los clientes se quejen de ella, o para que el monitoreo empiece a marcarla. Se trata de una cadena de métodos: quince llamadas profundas, filtros aplicados condicionalmente basados en los parámetros de la solicitud, un par de joins añadidos en algún lugar en una clase de repositorio base, carga ansiosa configurada en un archivo de modelo que no ha abierto en un tiempo. Usted habilita el registro de consultas y captura el SQL. Tiene 60 líneas. Tiene una subcuestión que no recuerda escribir. Hay un 'ORDER BY' en una columna que no está indexada. Ahora tienes que averiguar de dónde proviene esa subcuestión.¿Cuál de las quince llamadas de cadena de métodos la produjeron?¿Viene de la clase básica o del niño? Este es el impuesto oculto de la complejidad de ORM. Cuando algo va mal, está debugando dos capas simultáneamente: la lógica de la aplicación y la lógica de la generación de consultas. // What you see in code review $users = $this->userRepository ->createQueryBuilder('u') ->leftJoin('u.account', 'a') ->andWhere('u.isActive = :active') ->andWhere('a.plan = :plan') ->setParameter('active', true) ->setParameter('plan', $plan) ->getQuery() ->getResult(); // What actually runs. And nobody reads until something breaks SELECT u.id, u.name, u.email, u.created_at, u.updated_at, u.is_active, u.account_id, a.id AS a_id, a.name AS a_name, a.plan AS a_plan, a.is_active AS a_is_active, a.created_at AS a_created_at, a.updated_at AS a_updated_at, a.meta AS a_meta FROM users u LEFT JOIN accounts a ON a.id = u.account_id WHERE u.is_active = 1 AND a.plan = 'enterprise' Ese comportamiento de 'SELECT *' - arrastrando cada columna de cada tabla unida a la memoria - es lo que estás pagando cuando usas la hidratación de la entidad completa. A menudo, sólo necesitabas tres de esas veinte columnas. La arquitectura que realmente funciona Después de diez años de hacer y observar estos errores, aquí está la estructura que apliqué a los proyectos en los que estoy trabajando. ORM owns: - Todas las definiciones de esquema y migraciones Gestión del ciclo de vida de la entidad (crear, persistir, borrar, eliminar) - Buscas simples: encontrar por ID, encontrar por filtro único, listas de entidades paginadas - Gestión de relaciones dentro de conjuntos de resultados limitados y pequeños Raw SQL owns: - Consultas destinadas a informes, dashboards o exportaciones - Todas las operaciones en masa: inserciones de lote, actualizaciones de lote, upserts - Agregación a través de múltiples tablas - Cualquier consulta que utilice características específicas de la base de datos - Y muy a menudo, cualquier consulta que aparezca en el log de la consulta lenta En Symfony, la implementación de esta separación es limpia. repositorios de entidades manejan la capa ORM. Un conjunto separado de clases de consulta -los llamo "repositorios de lectura" o "repositorios de consulta" - manejan la capa SQL utilizando DBAI directamente: // Entity repository - pure ORM, simple operations class UserRepository extends ServiceEntityRepository { public function findActiveById(int $id): ?User { return $this->findOneBy(['id' => $id, 'isActive' => true]); } public function save(User $user): void { $this->getEntityManager()->persist($user); $this->getEntityManager()->flush(); } } // Query repository - raw SQL, complex reads class UserAnalyticsRepository { public function __construct(private readonly Connection $db) {} public function getRetentionBySignupCohort(\DateTimeImmutable $from, \DateTimeImmutable $to): array { $sql = <<<SQL SELECT DATE_FORMAT(u.created_at, '%Y-%m') AS cohort, COUNT(DISTINCT u.id) AS total_users, COUNT(DISTINCT CASE WHEN u.last_active_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN u.id END) AS active_last_30d, ROUND( COUNT(DISTINCT CASE WHEN u.last_active_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN u.id END) / COUNT(DISTINCT u.id) * 100, 1 ) AS retention_pct FROM users u WHERE u.created_at BETWEEN :from AND :to GROUP BY cohort ORDER BY cohort ASC SQL; return $this->db->fetchAllAssociative($sql, [ 'from' => $from->format('Y-m-d'), 'to' => $to->format('Y-m-d'), ]); } } El DBAL le da todo lo que necesita - declaraciones preparadas, gestión de conexión, soporte de transacciones. No está renunciando a la seguridad para escribir SQL. Está renunciando a la hidratación de objetos, que no necesitaba para esta consulta de todos modos. Tenga en cuenta también lo que la versión SQL cruda le da que la versión ORM no puede: la consulta está ahí. Cualquier desarrollador en el equipo puede leerla, comprenderla, copiarla en una consola de base de datos, ejecutar 'EXPLAIN' en ella y ajustarla. La habilidad que nunca fue opcional Algunas personas piensan que SQL es una habilidad heredada, algo que vuelves a tener cuando las herramientas modernas te fallan, como saber cómo usar un mapa físico porque tu teléfono ha muerto. Creo que esto lo lleva hacia atrás. SQL es el idioma que habla su base de datos. La base de datos está haciendo más trabajo que cualquier otro componente en la mayoría de las aplicaciones de backend. Comprender lo que le está pidiendo que haga no es experiencia opcional. Es fundamental. Los desarrolladores que más respeto en mi carrera comparten algo en común: se sienten cómodos en cada capa. Usan el ORM cuando se ajusta y escriben SQL cuando no. No sienten que alcanzar SQL es una admisión de derrota. Sienten que está usando la herramienta correcta. Y saben la herramienta correcta porque entienden ambas. No nos importa cómo se creó la consulta final SQL. Nos importa si puede usar un índice, cuántas filas tiene que examinar y cuántos datos tiene que mover.