Esta comparación de SQL y NoSQL es el siguiente paso después de convertir tu base de datos SQL Server a Couchbase. En el puesto anteriorCopié AdventureWorks de SQL Server a Couchbase.
En este post, voy a mostrar una aplicación ASP.NET Core que usa SQL Server, y cómo esa misma aplicación usaría Couchbase. Si quieres seguir el tutorial, puedes consultar la sección Proyecto SqlServerToCouchbase en GitHub.
A diferencia del post anterior, no estoy haciendo ningún intento de conversión "automática" de una aplicación. En su lugar, piensa en esto más como una comparación de SQL y NoSQL a nivel de aplicación.
Aplicaciones ASP.NET SQL Server
He creado una aplicación ASP.NET Core REST API muy simple. He utilizado Entity Framework, pero si estás usando Dapper, ADO.NET, NHibernate, etc, todavía debe ser capaz de seguir a lo largo.
Cada punto final devuelve JSON. También he añadido Swashbuckle al proyecto, para que puedas realizar peticiones directamente desde tu navegador a través de OpenAPI.
Aplicación ASP.NET Couchbase Server
La versión Couchbase de la aplicación devuelve los mismos datos, porque está utilizando los mismos datos SQL Server AdventureWorks.
En la aplicación, estoy utilizando el SDK .NET de Couchbase y Transacciones Couchbase bibliotecas. (Puede utilizar Linq2Base como un tipo de sustitución de Entity Framework).
Por lo demás, la aplicación es la misma, proporcionando una comparación (y contraste) entre SQL y NoSQL. Los endpoints devuelven JSON, y Swashbuckle está instalado.
Hay un controlador en cada muestra. Vamos a pasar por cada punto final en el controlador y realizar una comparación SQL y NoSQL.
Comparación entre SQL y NoSQL: Obtener por ID
Empecemos por el GetPersonByIdAsync
punto final. Dado un ID de persona, este punto final devuelve los datos de Persona para el ID dado.
Servidor SQL
Aquí está el ejemplo de SQL Server utilizando Entity Framework:
1 2 3 4 5 6 7 8 |
[HttpGet("/persona/{personId}")] público async Tarea<IActionResult> GetPersonByIdAsync(int personId) { var persona = await _contexto.Personas .SingleOrDefaultAsync(p => p.BusinessEntityID == personId); devolver Ok(persona); } |
También escribí otra versión de este método, llamada GetPersonByIdRawAsync
que utiliza una consulta SQL "en bruto". Esta consulta es muy similar a la que Entity Framework (arriba) genera en última instancia, y es similar a un enfoque Dapper.
1 2 3 4 5 6 7 8 9 |
[HttpGet("/personRaw/{personId}")] público async Tarea<IActionResult> GetPersonByIdRawAsync(int personId) { var persona = await _contexto.Personas .FromSqlRaw(@"SELECT * FROM Person.Person WHERE BusinessEntityID = {0}", personId) .SingleOrDefaultAsync(); devolver Ok(persona); } |
Tenga en cuenta que, de cualquier forma, se está ejecutando una consulta SQL.
Con N1QL, podríamos consultar los datos en Couchbase de una manera muy similar. Aquí está el GetPersonByIdRawAsync
en el proyecto Couchbase:
1 2 3 4 5 6 7 8 9 10 |
[HttpGet("/personRaw/{personId}")] público async Tarea<IActionResult> GetPersonByIdRawAsync(int personId) { var cubo = await _bucketProvider.GetBucketAsync(); var grupo = cubo.Grupo; var personaResultado = await grupo.QueryAsync<Persona>(@" SELECT p.* FROM AdventureWorks2016.Person.Person p WHERE p.BusinessEntityID = $personId", nuevo Opciones de consulta().Parámetro("personId", personId)); devolver Ok(await personaResultado.Filas.SingleOrDefaultAsync()); } |
(Hay un paso extra al pasar de "bucket" a "cluster". Esto podría ser omitido, pero yo uso cubo en otras partes del controlador, así que lo dejé en).
Sin embargo, el uso de una consulta N1QL implica una sobrecarga adicional (indexación, análisis de consultas, etc). Con Couchbase, si ya conocemos el ID de Persona, podemos saltarnos una consulta N1QL y hacer una búsqueda directa clave/valor (K/V).
Obtener por ID con K/V
La clave ya se conoce; se da como argumento. En lugar de utilizar SQL, vamos a hacer una búsqueda clave/valor. Hice esto en un método endpoint llamado GetPersonByIdAsync
:
1 2 3 4 5 6 7 8 9 |
[HttpGet("/persona/{personId}")] público async Tarea<IActionResult> GetPersonByIdAsync(int personId) { var cubo = await _bucketProvider.GetBucketAsync(); var alcance = await cubo.ScopeAsync("Persona"); var coll = await alcance.ColecciónAsync("Persona"); var personaDoc = await coll.GetAsync(personId.ToString()); devolver Ok(personaDoc.ContenidoComo<Persona>()); } |
A diferencia de SQL Server, Couchbase soporta una variedad de APIs para interactuar con los datos. En este caso, la búsqueda clave/valor extraerá el documento Persona directamente de la memoria. No hay necesidad de parsear una consulta SQL o usar cualquier indexación. Las búsquedas de clave/valor en Couchbase a menudo se miden en microsegundos.
Mi consejo: utiliza la búsqueda clave/valor siempre que puedas.
Obtener una entidad expandida por ID
Los datos pueden ser complejos y abarcar múltiples tablas (o múltiples documentos en el caso de Couchbase). Dependiendo de las herramientas que utilices, es posible que tengas alguna funcionalidad que pueda cargar entidades relacionadas.
Por ejemplo, con Entity Framework, puede utilizar una directiva Incluya
para extraer entidades relacionadas, como se muestra en este GetPersonByIdExpandedAsync
ejemplo:
1 2 3 4 5 6 7 8 9 |
[HttpGet("/personExpanded/{personId}")] público async Tarea<IActionResult> GetPersonByIdExpandedAsync(int personId) { var persona = await _contexto.Personas .Incluya(p => p.Direcciones de correo electrónico) .SingleOrDefaultAsync(p => p.BusinessEntityID == personId); devolver Ok(persona); } |
Entre bastidores, Entity Framework puede generar un JOIN y/o múltiples consultas SELECT para que esto suceda.
Aquí es donde cualquier O/RM (no sólo Entity Framework) puede ser peligroso. Asegúrese de utilizar una herramienta como SQL Profiler para ver qué consultas se están ejecutando realmente.
Nota
|
Los O/RM pueden ayudar, pero en un De SQL a NoSQL en comparación, es importante recordar que la incompatibilidad de impedancias es un problema mucho menor en el mundo NoSQL. |
Para el ejemplo de Couchbase, no estoy utilizando Entity Framework, pero en su lugar puedo utilizar el módulo Sintaxis NEST que forma parte de las extensiones N1QL del estándar SQL. Así es como la versión Couchbase de GetPersonByIdExpandedAsync
parece:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[HttpGet("/personExpanded/{personId}")] público async Tarea<IActionResult> GetPersonByIdExpandedAsync(int personId) { var cubo = await _bucketProvider.GetBucketAsync(); var grupo = cubo.Grupo; var personaResultado = await grupo.QueryAsync<Persona>(@" SELECT p.*, DireccionesEmail FROM AdventureWorks2016.Person.Persona p NEST AdventureWorks2016.Person.EmailAddresses EmailAddresses ON EmailAddresses.BusinessEntityID = p.BusinessEntityID WHERE p.BusinessEntityID = $personId", nuevo Opciones de consulta().Parámetro("personId", personId)); devolver Ok(await personaResultado.Filas.SingleOrDefaultAsync()); } |
NEST es un tipo de JOIN que coloca los datos JOINed en un objeto JSON anidado. En lugar de utilizar un O/RM para mapear los datos, estos datos pueden serializarse directamente en objetos C#.
Consulta de paginación
Veamos un ejemplo en el que NO tenemos una única clave para buscar un dato. Veamos un método que devuelve una "página" de resultados (quizás para rellenar una cuadrícula o lista de la interfaz de usuario).
Paginación en SQL Server
Esta es la versión de SQL Server de GetPersonsPageAsync
:
1 2 3 4 5 6 7 8 9 10 11 12 |
[HttpGet("/personas/página/{pageNum}")] público async Tarea<IActionResult> GetPersonsPageAsync(int pageNum) { var pageSize = 10; var personaPágina = await _contexto.Personas .OrderBy(p => p.Apellido) .Saltar(pageNum * pageSize) .Toma(pageSize) .Seleccione(p => nuevo { p.BusinessEntityID, p.Nombre, p.Apellido }) .ToListAsync(); devolver Ok(personaPágina); } |
Con Entity Framework, OrderBy
, Saltar
y Toma
se utilizan normalmente para la paginación. Si abrimos SQL Server Profiler, el SQL que esto genera se parece a esto:
1 2 3 4 |
exec sp_executesql N'SELECT [p].[BusinessEntityID], [p].[FirstName], [p].[LastName] FROM [Persona].[Persona] AS [p] ORDER BY [p].[Apellido] OFFSET @__p_0 ROWS FETCH NEXT @__p_0 ROWS ONLY',N'@__p_0 int',@__p_0=10 |
OFFSET ... ROWS FETCH NEXT ...
es la sintaxis que se utiliza aquí para la paginación.
Paging en Couchbase
La sintaxis de paginación siempre varía entre implementaciones SQL. Couchbase se inclina más hacia la sintaxis Oracle/MySQL en este aspecto. Aquí está la versión de Couchbase de GetPersonsPageAsync
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[HttpGet("/personas/página/{pageNum}")] público async Tarea<IActionResult> GetPersonsPageAsync(int pageNum) { var pageSize = 10; var cubo = await _bucketProvider.GetBucketAsync(); var bucketName = cubo.Nombre; var grupo = cubo.Grupo; var personaPágina = await grupo.QueryAsync<Persona>($@" SELECT p.LastName, p.BusinessEntityID, p.FirstName FROM `{bucketName}`.Person.Person p WHERE p.Apellidos NO FALTA ORDER BY p.LastName LIMIT {tamañoPágina} OFFSET {(pageNum * pageSize)} "); devolver Ok(await personaPágina.Filas.ToListAsync()); } |
En este caso, LÍMITE ... OFFSET ...
se está utilizando.
También quiero señalar la WHERE p.Apellidos NO FALTA
. Dado que Couchbase es una base de datos NoSQL, el motor de consulta no puede asumir que Apellido
estará en todos los documentos, incluso con ORDER BY p.LastName
. Al añadir este DONDE
la consulta sabe qué índice debe utilizar. Sin esto, la consulta tardará mucho más en ejecutarse.
Actualización con una transacción ACID
Con el modelo de estilo relacional que estamos utilizando tanto en SQL Server como en Couchbase para este ejemplo, las transacciones ACID serán importantes para ambas aplicaciones.
En estos ejemplos, hay un PersonUpdateApi
que permitirá al usuario actualizar ambos el nombre de una persona y su dirección de correo electrónico. Dado que estos datos están en dos tablas/filas separadas (SQL Server) o dos documentos separados (Couchbase), queremos que sea una operación atómica de todo o nada.
Nota
|
Se especifica un ID para ambas (para simplificar la API), ya que es posible (aunque poco frecuente en este conjunto de datos) que una persona tenga varias direcciones de correo electrónico. |
ACID con Entity Framework
A continuación se muestra un ejemplo de una transacción ACID que utiliza Entity Framework para actualizar tanto una fila de datos de la tabla Persona como una fila de datos de la tabla EmailAddress.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
[HttpPut("/persona")] público async Tarea<IActionResult> UpdatePurchaseOrderAsync(PersonUpdateApi personUpdateApi) { var transacción = await _contexto.Base de datos.BeginTransactionAsync(); pruebe { // encontrar a la persona var persona = await _contexto.Personas .Incluya(p => p.Direcciones de correo electrónico) .SingleOrDefaultAsync(p => p.BusinessEntityID == personUpdateApi.PersonId); // actualizar nombre persona.Nombre = personUpdateApi.Nombre; persona.Apellido = personUpdateApi.Apellido; // obtener la dirección de correo electrónico concreta y actualizarla // si el ID proporcionado no es válido, se lanzará una excepción var correo electrónico = persona.Direcciones de correo electrónico.Único(e => e.EmailAddressID == personUpdateApi.EmailAddressId); correo electrónico.Dirección de correo electrónico = personUpdateApi.Dirección de correo electrónico; await _contexto.SaveChangesAsync(); // confirmar transacción await transacción.CommitAsync(); devolver Ok($"Persona {personUpdateApi.PersonId} nombre y correo electrónico actualizados".); } captura (Excepción ex) { await transacción.RollbackAsync(); devolver BadRequest("Algo salió mal, la transacción retrocedió"); } } |
Tenga en cuenta las cuatro partes principales de una transacción:
- Iniciar transacción (
_context.Database.BeginTransactionAsync();
) pruebe
/captura
- Transacción de compromiso (
await transacción.CommitAsync();
) - Transacción de Rollback en el
captura
(transacción.RollbackAsync();
)
Esta es una característica importante en la que la comparación entre SQL y NoSQL ha cambiado en los últimos años. Con Couchbase, las transacciones ACID son ahora posibles.
ACID con una transacción Couchbase
Con Couchbase, la API es ligeramente diferente, pero se siguen los mismos pasos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
[HttpPut("/persona")] público async Tarea<IActionResult> UpdatePurchaseOrderAsync(PersonUpdateApi personUpdateApi) { // configurar bucket, cluster y colecciones var cubo = await _bucketProvider.GetBucketAsync(); var alcance = await cubo.ScopeAsync("Persona"); var personaColl = await alcance.ColecciónAsync("Persona"); var emailColl = await alcance.ColecciónAsync("Dirección de correo electrónico); // crear transacción var grupo = cubo.Grupo; var transacción = Transacciones.Cree(grupo, TransactionConfigBuilder.Cree() .Nivel de durabilidad(Nivel de durabilidad.Ninguno) .Construya()); pruebe { await transacción.RunAsync(async (contexto) => { // actualizar documentos de persona y correo electrónico // en función de los valores introducidos en el objeto API var personKey = personUpdateApi.PersonId.ToString(); var emailClave = personKey + "::" + personUpdateApi.EmailAddressId.ToString(); var persona = await contexto.GetAsync(personaColl, personKey); var correo electrónico = await contexto.GetAsync(emailColl, emailClave); var personaDoc = persona.ContenidoComo<dinámico>(); var emailDoc = correo electrónico.ContenidoComo<dinámico>(); personaDoc.Nombre = personUpdateApi.Nombre; personaDoc.Apellido = personUpdateApi.Apellido; emailDoc.Dirección de correo electrónico = personUpdateApi.Dirección de correo electrónico; await contexto.ReplaceAsync(persona, personaDoc); await contexto.ReplaceAsync(correo electrónico, emailDoc); }); devolver Ok($"Persona {personUpdateApi.PersonId} nombre y correo electrónico actualizados".); } captura (Excepción ex) { devolver BadRequest("Algo salió mal, la transacción retrocedió".); } } |
Los mismos pasos principales son:
- Iniciar transacción (
transaction.RunAsync( ... )
) pruebe
/captura
- Transacción de compromiso (implícita, pero
context.CommitAsync()
podría utilizarse) - Transacción de reversión (de nuevo, implícita, pero
context.RollbackAsync()
).
En ambos casos, tenemos una transacción ACID. A diferencia de SQL Server, sin embargo, podemos más tarde optimizar y consolidar los datos en Couchbase para reducir la cantidad de transacciones ACID que necesitamos y aumentar el rendimiento.
Procedimientos almacenados: comparación entre SQL y NoSQL
Los procedimientos almacenados son un tema a veces controvertido. En general, pueden contener mucha funcionalidad y lógica.
Procedimiento almacenado en SQL Server
He creado un procedimiento almacenado llamado "ListSubcomponents" (puede ver el archivo todos los detalles en GitHub). Con Entity Framework, puede utilizar FromSqlRaw
para ejecutarlo y asignar los resultados a objetos C#. He creado un objeto C# pseudo-entidad llamado ListaSubcomponentes
que se utiliza sólo para este sproc:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// sproc ejemplo - ver ExampleStoredProcedure.sql [HttpGet("/getListSubcomponents/{listPriceMin}/{listPriceMax}")] público async Tarea<IActionResult> GetListSubcomponents(decimal listPriceMin, decimal listPriceMax) { var listPriceMinParam = nuevo ParámetroSql("@ListPriceMin", SqlDbType.Decimal) {Valor = listPriceMin }; var listPriceMaxParam = nuevo ParámetroSql("@ListPriceMax", SqlDbType.Decimal) {Valor = listPriceMax }; var resultado = await _contexto.ListaSubcomponentes .FromSqlRaw("EJECUTAR dbo.ListaSubcomponentes @ListPrecioMin, @ListPrecioMax", listPriceMinParam, listPriceMaxParam) .ToListAsync(); devolver Ok(resultado); } |
El procedimiento almacenado tiene dos parámetros.
Couchbase Función definida por el usuario
Couchbase no tiene nada llamado "procedimiento almacenado" (todavía), pero tiene algo llamado función definida por el usuario (UDF) que también puede contener lógica compleja cuando sea necesario.
He creado una UDF llamada ListaSubcomponentes
(que también puede ver en GitHub) que coincide con la funcionalidad del sproc de SQL Server.
A continuación se explica cómo ejecutar esa UDF desde ASP.NET:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// sproc ejemplo - ver ExampleStoredProcedure.sql [HttpGet("/getListSubcomponents/{listPriceMin}/{listPriceMax}")] público async Tarea<IActionResult> GetListSubcomponents(decimal listPriceMin, decimal listPriceMax) { var cubo = await _bucketProvider.GetBucketAsync(); var grupo = cubo.Grupo; var opciones = nuevo Opciones de consulta(); opciones.Parámetro("$listPriceMin", listPriceMin); opciones.Parámetro("$listPriceMax", listPriceMax); var resultado = await grupo.QueryAsync<ListaSubcomponente>( "SELECT l.* FROM ListSubcomponents($listPriceMin, $listPriceMax) l", opciones); devolver Ok(await resultado.Filas.ToListAsync()); } |
Invocarlo en Couchbase con dos parámetros es muy similar a usar FromSqlRaw con Entity Framework.
Rendimiento - Comparación de SQL con NoSQL
Ahora que he convertido la aplicación para utilizar Couchbase, ¿la nueva versión funciona al menos tan rápido como la antigua versión de SQL Server?
Es una pregunta complicada de responder porque:
- No he hecho NINGUNA optimización en el modelo de datos. Todavía estoy usando la conversión literal de datos de el puesto anterior.
- El acceso a los datos puede variar mucho de un caso de uso a otro.
- Los entornos pueden variar mucho de una persona a otra y de una empresa a otra.
Sin embargo, quería hacer algunas pruebas de carga para comprobar que todo está bien.
Ejecuté ambas aplicaciones en mi máquina local y utilicé ngrok para exponerlos a Internet. A continuación, utilicé loader.io (una excelente herramienta para pruebas de carga con concurrencia). A continuación, corrí algunas pruebas rápidas de rendimiento contra sólo el punto final 'paginación'. Este es el punto final que más me preocupa para el rendimiento, y también creo que es el más "manzanas con manzanas" SQL y NoSQL comparación entre los puntos finales.
Comparación de pruebas de carga SQL y NoSQL
Estos son los resultados de la aplicación SQL Server:
Y aquí están los resultados de la aplicación Couchbase Server:
Interpretación de los resultados de la prueba de carga comparativa SQL y NoSQL
Esto no pretende ser un punto de referencia o un dato que diga "Couchbase es más rápido que SQL Server".
Sólo pretende ser una comprobación de cordura.
Si no obtengo al menos el mismo rendimiento bajo carga que antes, quizá esté haciendo algo mal. Este es un beneficio crucial para el prueba de concepto proceso. Aunque Couchbase, especialmente Couchbase 7, es muy amigable con las relaciones, todavía hay diferencias y matices entre cada y este proceso le ayudará a identificar las diferencias más importantes para usted y su proyecto.
Si buscas puntos de referencia más sólidos, aquí tienes algunos recursos que puedes consultar:
- Informes comparativos de Altoros (3ª parte)
- Puntos de referencia en la nube
- Servidor Couchbase "Puntos de referencia "ShowFast
Conclusión
La comparación de SQL y NoSQL y la conversión del código de la aplicación, combinadas con algunas pruebas de carga muy básicas, me demuestran que sí:
- Alojar un modelo de datos relacional tal cual, sin cambios de modelado
- Convertir punto(s) final(es) ASP.NET para utilizar el SDK de Couchbase
- Se espera un rendimiento al menos igual de bueno al principio, con mucho margen para escalar y mejorar, con un riesgo bajo.
Tu caso de uso puede variar, pero recuerda también que durante esta conversión, Couchbase nos dio:
- Fácil escalabilidad horizontal
- Alta disponibilidad
- Almacenamiento en caché integrado
- Flexibilidad del esquema (que es probablemente la razón por la que estás buscando utilizar Couchbase en primer lugar).
Anexo
He aquí una guía sucinta de la comparación entre SQL y NoSQL que realicé en la aplicación.
Funcionamiento de SQL Server | Operación Couchbase |
---|---|
Lectura/escritura de una fila/entidad |
|
Lectura/escritura de varias filas/páginas |
|
SELECCIONE una entidad con entidades relacionadas |
Consulta N1QL con NEST |
IniciarTransacción |
|
Procedimiento almacenado |
Recordatorios:
- Cambie a la API de clave/valor cuando pueda
- Utilizar la indexación, la visualización del plan de indexación y el asesor de índices al escribir N1QL
- Utilice una transacción ACID (únicamente) cuando necesite
- Piense en los objetivos de rendimiento y establezca una forma de ponerlos a prueba.
Próximos pasos
Echa un vistazo Couchbase Server 7, actualmente en fase betahoy. Es una descarga gratuita. Pruebe a cargar sus datos relacionales en él, convirtiendo algunos puntos finales, y vea si el proceso le funciona.