Essa comparação entre SQL e NoSQL é a próxima etapa após a conversão do seu banco de dados do SQL Server para o Couchbase. Em a postagem anteriorEm um dos meus testes, copiei o AdventureWorks do SQL Server para o Couchbase.
Nesta postagem, mostrarei um aplicativo ASP.NET Core que usa o SQL Server e como esse mesmo aplicativo usaria o Couchbase. Se quiser acompanhar, você pode conferir o Projeto SqlServerToCouchbase no GitHub.
Ao contrário da postagem anterior, não estou tentando fazer uma conversão "automática" de um aplicativo. Em vez disso, pense nisso mais como uma comparação entre SQL e NoSQL no nível do aplicativo.
Aplicativos ASP.NET SQL Server
Criei um aplicativo muito simples no estilo da API REST do ASP.NET Core. Usei o Entity Framework, mas se você estiver usando Dapper, ADO.NET, NHibernate etc., ainda poderá acompanhar o processo.
Cada endpoint retorna JSON. Também adicionei o Swashbuckle ao projeto, para que você possa emitir solicitações diretamente do navegador via OpenAPI.
Aplicativo do servidor ASP.NET Couchbase
A versão Couchbase do aplicativo retorna os mesmos dados, porque está usando os mesmos dados do SQL Server AdventureWorks.
No aplicativo, estou usando o SDK do Couchbase .NET e Transações do Couchbase bibliotecas. (Você poderia usar Linq2Couchbase como um tipo de substituição do Entity Framework).
Caso contrário, o aplicativo é o mesmo, fornecendo uma comparação (e contraste) entre SQL e NoSQL. Os pontos de extremidade estão retornando JSON, e o Swashbuckle está instalado.
Há um controlador em cada amostra. Vamos examinar cada endpoint no controlador e realizar uma comparação SQL e NoSQL.
Comparação entre SQL e NoSQL: Obter por ID
Vamos começar com o GetPersonByIdAsync
endpoint. Dado um ID de pessoa, esse ponto de extremidade retorna os dados da pessoa para o ID fornecido.
SQL Server
Aqui está o exemplo do SQL Server usando o Entity Framework:
1 2 3 4 5 6 7 8 |
[HttpGet("/person/{personId}")] público assíncrono Tarefa<IActionResult> GetPersonByIdAsync(int personId) { var pessoa = aguardar _contexto.Pessoas .SingleOrDefaultAsync(p => p.BusinessEntityID == personId); retorno Ok(pessoa); } |
Também escrevi outra versão desse método, chamada GetPersonByIdRawAsync
que usa uma consulta SQL "bruta". Essa consulta é muito parecida com a que o Entity Framework (acima) gera, e é semelhante a uma abordagem Dapper.
1 2 3 4 5 6 7 8 9 |
[HttpGet("/personRaw/{personId}")] público assíncrono Tarefa<IActionResult> GetPersonByIdRawAsync(int personId) { var pessoa = aguardar _contexto.Pessoas .FromSqlRaw(@"SELECT * FROM Person.Person WHERE BusinessEntityID = {0}", personId) .SingleOrDefaultAsync(); retorno Ok(pessoa); } |
Observe que, de qualquer forma, uma consulta SQL está sendo executada.
Com o N1QL, podemos consultar os dados no Couchbase de forma muito semelhante. Aqui está o GetPersonByIdRawAsync
no projeto Couchbase:
1 2 3 4 5 6 7 8 9 10 |
[HttpGet("/personRaw/{personId}")] público assíncrono Tarefa<IActionResult> GetPersonByIdRawAsync(int personId) { var balde = aguardar _bucketProvider.GetBucketAsync(); var agrupamento = balde.Aglomerado; var personResult = aguardar agrupamento.QueryAsync<Pessoa>(@" SELECT p.* FROM AdventureWorks2016.Person.Person p WHERE p.BusinessEntityID = $personId", novo Opções de consulta().Parâmetro("personId", personId)); retorno Ok(aguardar personResult.Fileiras.SingleOrDefaultAsync()); } |
(Há uma etapa extra que vai de "bucket" para "cluster". Ela poderia ser ignorada, mas como uso bucket em outras partes do controlador, deixei-a lá).
No entanto, o uso de uma consulta N1QL envolve alguma sobrecarga extra (indexação, análise de consulta etc.). Com o Couchbase, se já soubermos o ID da pessoa, podemos ignorar uma consulta N1QL e fazer uma pesquisa direta de chave/valor (K/V).
Obter por ID com K/V
A chave já é conhecida; ela é fornecida como um argumento. Em vez de usar SQL, vamos fazer uma pesquisa de chave/valor. Fiz isso em um método de endpoint chamado GetPersonByIdAsync
:
1 2 3 4 5 6 7 8 9 |
[HttpGet("/person/{personId}")] público assíncrono Tarefa<IActionResult> GetPersonByIdAsync(int personId) { var balde = aguardar _bucketProvider.GetBucketAsync(); var escopo = aguardar balde.ScopeAsync("Pessoa"); var col = aguardar escopo.CollectionAsync("Pessoa"); var pessoaDoc = aguardar col.GetAsync(personId.ToString()); retorno Ok(pessoaDoc.ContentAs<Pessoa>()); } |
Ao contrário do SQL Server, o Couchbase oferece suporte a uma variedade de APIs para interagir com os dados. Nesse caso, a pesquisa de chave/valor estará extraindo o documento Person diretamente da memória. Não há necessidade de analisar uma consulta SQL ou usar qualquer indexação. As pesquisas de chave/valor no Couchbase geralmente são medidas em microssegundos.
Minha recomendação: use a pesquisa de chave/valor sempre que possível.
Obter uma entidade expandida por ID
Os dados podem ser complexos e abranger várias tabelas (ou vários documentos, no caso do Couchbase). Dependendo das ferramentas que estiver usando, você pode ter alguma funcionalidade que possa carregar entidades relacionadas.
Por exemplo, com o Entity Framework, você pode usar um Incluir
para atrair entidades relacionadas, como mostrado neste GetPersonByIdExpandedAsync
exemplo:
1 2 3 4 5 6 7 8 9 |
[HttpGet("/personExpanded/{personId}")] público assíncrono Tarefa<IActionResult> GetPersonByIdExpandedAsync(int personId) { var pessoa = aguardar _contexto.Pessoas .Incluir(p => p.Endereços de e-mail) .SingleOrDefaultAsync(p => p.BusinessEntityID == personId); retorno Ok(pessoa); } |
Nos bastidores, o Entity Framework pode gerar uma consulta JOIN e/ou várias consultas SELECT para que isso aconteça.
É nesse ponto que qualquer O/RM (não apenas o Entity Framework) pode ser perigoso. Certifique-se de usar uma ferramenta como o SQL Profiler para ver quais consultas estão realmente sendo executadas.
Observação
|
Os O/RMs podem ajudar, mas em um SQL para NoSQL Em comparação, é importante lembrar que a incompatibilidade de impedância é um problema muito menor no mundo NoSQL. |
Para o exemplo do Couchbase, não estou usando o Entity Framework, mas, em vez disso, posso usar o Sintaxe NEST que faz parte das extensões N1QL do padrão SQL. Veja como a versão do Couchbase do GetPersonByIdExpandedAsync
aparência:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[HttpGet("/personExpanded/{personId}")] público assíncrono Tarefa<IActionResult> GetPersonByIdExpandedAsync(int personId) { var balde = aguardar _bucketProvider.GetBucketAsync(); var agrupamento = balde.Aglomerado; var personResult = aguardar agrupamento.QueryAsync<Pessoa>(@" SELECT p.*, EmailAddresses FROM AdventureWorks2016.Person.Person p NEST AdventureWorks2016.Person.EmailAddresses EmailAddresses ON EmailAddresses.BusinessEntityID = p.BusinessEntityID WHERE p.BusinessEntityID = $personId", novo Opções de consulta().Parâmetro("personId", personId)); retorno Ok(aguardar personResult.Fileiras.SingleOrDefaultAsync()); } |
NEST é um tipo de JOIN que coloca os dados JOINed em um objeto JSON aninhado. Em vez de usar um O/RM para mapear os dados, esses dados podem ser serializados diretamente em objetos C#.
Consulta de paginação
Vamos dar uma olhada em um exemplo em que NÃO temos uma única chave para procurar um dado. Vejamos um método que retorna uma "página" de resultados (talvez para preencher uma grade ou lista da interface do usuário).
Paginação no SQL Server
Aqui está a versão do SQL Server de GetPersonsPageAsync
:
1 2 3 4 5 6 7 8 9 10 11 12 |
[HttpGet("/persons/page/{pageNum}")] público assíncrono Tarefa<IActionResult> GetPersonsPageAsync(int pageNum) { var pageSize = 10; var pessoaPágina = aguardar _contexto.Pessoas .Ordem(p => p.Sobrenome) .Pular(pageNum * pageSize) .Pegue(pageSize) .Selecione(p => novo { p.BusinessEntityID, p.FirstName, p.Sobrenome }) .ToListAsync(); retorno Ok(pessoaPágina); } |
Com o Entity Framework, Ordem
, Pular
e Pegue
são normalmente usados para paginação. Se abrirmos o SQL Server Profiler, o SQL gerado terá a seguinte aparência:
1 2 3 4 |
executar sp_executesql N'SELECT [p].[BusinessEntityID], [p].[FirstName], [p].[LastName] FROM [Pessoa].[Pessoa] AS [p] ORDER BY [p].[LastName] OFFSET @__p_0 ROWS FETCH NEXT @__p_0 ROWS ONLY',N'@__p_0 int',@__p_0=10 |
OFFSET ... ROWS FETCH NEXT ...
é a sintaxe que está sendo usada para paginação aqui.
Paginação no Couchbase
A sintaxe de paginação sempre varia entre as implementações de SQL. O Couchbase se inclina mais para a sintaxe do Oracle/MySQL nesse aspecto. Aqui está a versão do Couchbase de GetPersonsPageAsync
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[HttpGet("/persons/page/{pageNum}")] público assíncrono Tarefa<IActionResult> GetPersonsPageAsync(int pageNum) { var pageSize = 10; var balde = aguardar _bucketProvider.GetBucketAsync(); var bucketName = balde.Nome; var agrupamento = balde.Aglomerado; var pessoaPágina = aguardar agrupamento.QueryAsync<Pessoa>($@" SELECT p.LastName, p.BusinessEntityID, p.FirstName FROM `{bucketName}`.Person.Person p WHERE p.LastName IS NOT MISSING ORDER BY p.LastName LIMIT {pageSize} OFFSET {(pageNum * pageSize)} "); retorno Ok(aguardar pessoaPágina.Fileiras.ToListAsync()); } |
Nesse caso, LIMITE ... DESLOCAMENTO ...
está sendo usado.
Também quero destacar o WHERE p.LastName IS NOT MISSING
. Como o Couchbase é um banco de dados NoSQL, o mecanismo de consulta não pode presumir que Sobrenome
estará em todos os documentos, mesmo com ORDER BY p.LastName
. Ao adicionar este ONDE
a consulta agora sabe qual índice usar. Sem isso, a consulta levará muito mais tempo para ser executada.
Atualizar com uma transação ACID
Com o modelo de estilo relacional que estamos usando no SQL Server e no Couchbase para este exemplo, as transações ACID serão importantes para ambos os aplicativos.
Nesses exemplos, há um PersonUpdateApi
que permitirá que o usuário atualize ambos o nome de uma pessoa e seu endereço de e-mail. Como esses dados estão em duas tabelas/linhas separadas (SQL Server) ou dois documentos separados (Couchbase), queremos que essa seja uma operação atômica do tipo tudo ou nada.
Observação
|
Um ID é especificado para ambos (para simplificar a API), pois é possível (mas raro nesse conjunto de dados) que uma pessoa tenha vários endereços de e-mail. |
ACID com o Entity Framework
Aqui está um exemplo de uma transação ACID usando o Entity Framework para atualizar uma linha de dados na tabela Person e uma linha de dados na tabela 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("/pessoa")] público assíncrono Tarefa<IActionResult> UpdatePurchaseOrderAsync(PersonUpdateApi personUpdateApi) { var transação = aguardar _contexto.Banco de dados.BeginTransactionAsync(); tentar { // encontrar a pessoa var pessoa = aguardar _contexto.Pessoas .Incluir(p => p.Endereços de e-mail) .SingleOrDefaultAsync(p => p.BusinessEntityID == personUpdateApi.PersonId); // atualizar nome pessoa.FirstName = personUpdateApi.FirstName; pessoa.Sobrenome = personUpdateApi.Sobrenome; // obter o endereço de e-mail específico e atualizá-lo // se a ID fornecida for inválida, isso lançará uma exceção var e-mail = pessoa.Endereços de e-mail.Individual(e => e.EmailAddressID == personUpdateApi.EmailAddressId); e-mail.Endereço de e-mail = personUpdateApi.Endereço de e-mail; aguardar _contexto.SaveChangesAsync(); // confirmar transação aguardar transação.CommitAsync(); retorno Ok($"Nome e e-mail da pessoa {personUpdateApi.PersonId} atualizados."); } captura (Exceção ex) { aguardar transação.RollbackAsync(); retorno BadRequest("Algo deu errado, a transação foi revertida"); } } |
Observe as quatro partes principais de uma transação:
- Iniciar transação (
_context.Database.BeginTransactionAsync();
) tentar
/captura
- Transação de confirmação (
await transaction.CommitAsync();
) - Transação de reversão no
captura
(transaction.RollbackAsync();
)
Esse é um recurso importante em que a comparação entre SQL e NoSQL mudou nos últimos anos. Com o Couchbase, as transações ACID agora são possíveis.
ACID com uma transação do Couchbase
Com o Couchbase, a API é um pouco diferente, mas as mesmas etapas estão todas lá:
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("/pessoa")] público assíncrono Tarefa<IActionResult> UpdatePurchaseOrderAsync(PersonUpdateApi personUpdateApi) { // configurar o bucket, o cluster e as coleções var balde = aguardar _bucketProvider.GetBucketAsync(); var escopo = aguardar balde.ScopeAsync("Pessoa"); var personColl = aguardar escopo.CollectionAsync("Pessoa"); var emailColl = aguardar escopo.CollectionAsync("Endereço de e-mail"); // criar transação var agrupamento = balde.Aglomerado; var transação = Transações.Criar(agrupamento, TransactionConfigBuilder.Criar() .Nível de durabilidade(Nível de durabilidade.Nenhum) .Construir()); tentar { aguardar transação.RunAsync(assíncrono (contexto) => { // atualizar documentos pessoais e de e-mail // com base nos valores passados no objeto da API var personKey = personUpdateApi.PersonId.ToString(); var emailKey = personKey + "::" + personUpdateApi.EmailAddressId.ToString(); var pessoa = aguardar contexto.GetAsync(personColl, personKey); var e-mail = aguardar contexto.GetAsync(emailColl, emailKey); var pessoaDoc = pessoa.ContentAs<dinâmico>(); var emailDoc = e-mail.ContentAs<dinâmico>(); pessoaDoc.FirstName = personUpdateApi.FirstName; pessoaDoc.Sobrenome = personUpdateApi.Sobrenome; emailDoc.Endereço de e-mail = personUpdateApi.Endereço de e-mail; aguardar contexto.ReplaceAsync(pessoa, pessoaDoc); aguardar contexto.ReplaceAsync(e-mail, emailDoc); }); retorno Ok($"Nome e e-mail da pessoa {personUpdateApi.PersonId} atualizados."); } captura (Exceção ex) { retorno BadRequest("Algo deu errado, a transação foi revertida".); } } |
As etapas principais são as mesmas:
- Iniciar transação (
transaction.RunAsync( ... )
) tentar
/captura
- Transação de compromisso (implícita, mas
contexto.CommitAsync()
poderia ser usado) - Transação de reversão (novamente, implícita, mas
contexto.RollbackAsync()
poderia ser usado).
Em ambos os casos, temos uma transação ACID. Diferente de SQL Server, no entanto, podemos mais tarde otimizar e consolidar os dados no Couchbase para reduzir a quantidade de transações ACID de que precisamos e aumentar o desempenho.
Procedimentos armazenados: uma comparação entre SQL e NoSQL
Os procedimentos armazenados são um tópico às vezes controverso. De modo geral, eles podem conter muita funcionalidade e lógica.
Procedimento armazenado no SQL Server
Criei um procedimento armazenado chamado "ListSubcomponents" (você pode ver o arquivo Detalhes completos no GitHub). Com o Entity Framework, você pode usar FromSqlRaw
para executá-lo e mapear os resultados para objetos C#. Criei um objeto C# de pseudo-entidade chamado ListSubcomponents
que é usado apenas para esse sproc:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// exemplo de sproc - consulte ExampleStoredProcedure.sql [HttpGet("/getListSubcomponents/{listPriceMin}/{listPriceMax}")] público assíncrono Tarefa<IActionResult> GetListSubcomponents(decimal listPriceMin, decimal listPriceMax) { var listPriceMinParam = novo SqlParameter("@ListPriceMin", SqlDbType.Decimal) {Valor = listPriceMin }; var listPriceMaxParam = novo SqlParameter("@ListPriceMax", SqlDbType.Decimal) {Valor = listPriceMax }; var resultado = aguardar _contexto.ListSubcomponents .FromSqlRaw("EXECUTE dbo.ListSubcomponents @ListPriceMin, @ListPriceMax", listPriceMinParam, listPriceMaxParam) .ToListAsync(); retorno Ok(resultado); } |
O procedimento armazenado tem dois parâmetros.
Função definida pelo usuário do Couchbase
O Couchbase não tem nada chamado de "procedimento armazenado" (ainda), mas tem algo chamado de função definida pelo usuário (UDF) que também pode conter lógica complexa quando necessário.
Criei um UDF chamado ListSubcomponents
(que você também pode visualização no GitHub) que corresponde à funcionalidade do sproc do SQL Server.
Veja a seguir como executar esse UDF no ASP.NET:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// exemplo de sproc - consulte ExampleStoredProcedure.sql [HttpGet("/getListSubcomponents/{listPriceMin}/{listPriceMax}")] público assíncrono Tarefa<IActionResult> GetListSubcomponents(decimal listPriceMin, decimal listPriceMax) { var balde = aguardar _bucketProvider.GetBucketAsync(); var agrupamento = balde.Aglomerado; var opções = novo Opções de consulta(); opções.Parâmetro("$listPriceMin", listPriceMin); opções.Parâmetro("$listPriceMax", listPriceMax); var resultado = aguardar agrupamento.QueryAsync<ListSubcomponent>( "SELECT l.* FROM ListSubcomponents($listPriceMin, $listPriceMax) l", opções); retorno Ok(aguardar resultado.Fileiras.ToListAsync()); } |
Invocá-lo no Couchbase com dois parâmetros é muito semelhante a usar o FromSqlRaw com o Entity Framework.
Desempenho - Comparação entre SQL e NoSQL
Agora que converti o aplicativo para usar o Couchbase, a nova versão é executada pelo menos tão rapidamente quanto a versão antiga do SQL Server?
É uma pergunta complicada de responder porque:
- Não fiz NENHUMA otimização no modelo de dados. Ainda estou usando a conversão literal de dados de a postagem anterior.
- O acesso aos dados pode variar muito de caso de uso para caso de uso.
- Os ambientes podem variar muito de pessoa para pessoa e de empresa para empresa.
No entanto, eu queria fazer alguns testes de carga "por trás do envelope" como uma verificação de sanidade.
Executei os dois aplicativos em minha máquina local e usei ngrok para expô-los à Internet. Em seguida, usei carregador.io (uma excelente ferramenta para testes de carga com concorrência). Em seguida, executei alguns testes rápidos de desempenho somente com o endpoint de "paginação". Esse é o endpoint com o qual estou mais preocupado em termos de desempenho, e também acho que é a comparação SQL e NoSQL mais "igualitária" entre os endpoints.
Comparação entre SQL e NoSQL para teste de carga
Aqui estão os resultados do aplicativo do SQL Server:
E aqui estão os resultados do aplicativo Couchbase Server:
Interpretação dos resultados do teste de carga de comparação entre SQL e NoSQL
Não se trata de um benchmark ou de um ponto de dados que diga que "o Couchbase é mais rápido que o SQL Server".
O objetivo é apenas uma verificação de sanidade.
Se eu não estiver obtendo um desempenho pelo menos tão bom sob carga quanto antes, talvez eu esteja fazendo algo errado. Esse é um benefício crucial para o prova de conceito processo. Embora o Couchbase, especialmente o Couchbase 7, seja muito compatível com o sistema relacional, ainda existem diferenças e nuances entre todos e esse processo o ajudará a identificar as diferenças que mais importam para você e seu projeto.
Se estiver procurando benchmarks mais robustos, aqui estão alguns recursos que você pode consultar:
- Relatórios de benchmark da Altoros (terceiros)
- Benchmarks de nuvem
- Servidor Couchbase "Benchmarks "ShowFast
Conclusão
A comparação e a conversão de SQL e NoSQL do código do aplicativo, combinadas com alguns testes de carga muito básicos, mostram que eu posso:
- Hospedar um modelo de dados relacional como está, sem alterações de modelagem
- Converter os pontos de extremidade do ASP.NET para usar o SDK do Couchbase
- Espere um desempenho pelo menos tão bom no início, com muito espaço para escalar e melhorar, com baixo risco.
Seu caso de uso pode variar, mas lembre-se também de que, durante essa conversão, o Couchbase nos forneceu:
- Fácil escalabilidade horizontal
- Alta disponibilidade
- Armazenamento em cache incorporado
- Flexibilidade do esquema (que é provavelmente o motivo pelo qual você está procurando usar o Couchbase em primeiro lugar).
Apêndice
Aqui está um guia sucinto da comparação entre SQL e NoSQL que fiz no aplicativo.
Operação do SQL Server | Operação do Couchbase |
---|---|
Ler/gravar uma linha/entidade |
|
Ler/gravar várias linhas/páginas |
|
SELECIONAR uma entidade com entidades relacionadas |
Consulta N1QL com NEST |
BeginTransaction |
|
Procedimento armazenado |
Lembretes:
- Mude para a API de chave/valor quando puder
- Use a indexação, a visualização do plano de indexação e o consultor de índices ao escrever N1QL
- Use uma transação ACID (somente) quando precisar
- Pense nas metas de desempenho e estabeleça uma maneira de testá-las
Próximas etapas
Confira Couchbase Server 7, atualmente em versão betahoje. É um download gratuito. Tente carregar seus dados relacionais nele, convertendo alguns pontos de extremidade, e veja se o processo funciona para você.