Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Este artigo baseia-se em vários conceitos do Azure Cosmos DB, como modelação de dados, particionamento e throughput provisionado, para demonstrar como abordar um exercício real de concepção de dados.
Se normalmente trabalha com bases de dados relacionais, provavelmente desenvolveu hábitos para desenhar modelos de dados. Devido às restrições específicas, mas também às forças únicas do Azure Cosmos DB, a maioria destas boas práticas não se traduz bem e pode arrastar-te para soluções subótimas. O objetivo deste artigo é guiá-lo através de todo o processo de modelação de um caso de uso real no Azure Cosmos DB, desde a modelação de itens até à colocation de entidades e particionamento de contentores.
Para um exemplo que ilustre os conceitos deste artigo, descarregue ou veja este código-fonte gerado pela comunidade.
Importante
Um colaborador da comunidade contribuiu com este exemplo de código. A equipa do Azure Cosmos DB não suporta a sua manutenção.
O cenário
Para este exercício, vamos considerar o domínio de uma plataforma de blogue onde os utilizadores podem criar publicações. Os utilizadores também podem gostar e adicionar comentários a essas publicações.
Sugestão
Algumas palavras são destacadas em itálico para identificar o tipo de "coisas" que o nosso modelo manipula.
Adicionando mais requisitos à nossa especificação:
- Uma página inicial mostra um feed de publicações recentemente criadas.
- Podemos obter todas as publicações de um utilizador, todos os comentários de uma publicação e todos os gostos de uma publicação.
- As publicações são devolvidas com o nome de utilizador dos seus autores e uma contagem de quantos comentários e gostos têm.
- Os comentários e gostos também são devolvidos com o nome de utilizador dos utilizadores que os criaram.
- Quando exibidos como listas, os posts só têm de apresentar um resumo truncado do seu conteúdo.
Identificar os principais padrões de acesso
Para começar, damos alguma estrutura à nossa especificação inicial identificando os padrões de acesso da nossa solução. Ao desenhar um modelo de dados para o Azure Cosmos DB, é importante perceber quais os pedidos que o nosso modelo tem de servir para garantir que o modelo serve esses pedidos de forma eficiente.
Para tornar o processo global mais fácil de seguir, categorizamos esses diferentes pedidos como comandos ou consultas, tomando algum vocabulário da segregação de responsabilidades por consultas de comandos (CQRS). No CQRS, comandos são requisições de escrita (isto é, intenções de atualizar o sistema) e consultas são requisições apenas de leitura.
Aqui está a lista de pedidos que a nossa plataforma expõe:
- [C1] Criar ou editar um utilizador
- [Q1] Recuperar um utilizador
- [C2] Crie ou edite uma publicação
- [Q2] Recuperar uma publicação
- [P3] Liste as publicações de um utilizador em formato curto
- [C3] Crie um comentário
- [Q4] Liste os comentários de uma publicação
- [C4] Gostar de uma publicação
- [Q5] Liste os likes de uma publicação
- [Q6] Lista os x posts mais recentes criados em formato curto (feed)
Nesta fase, ainda não pensámos nos detalhes do que cada entidade (utilizador, publicação, etc.) contém. Este passo é geralmente um dos primeiros a ser abordado ao desenhar contra uma loja relacional. Começamos por este passo porque temos de perceber como essas entidades se traduzem em termos de tabelas, colunas, chaves estrangeiras, e assim por diante. É muito menos preocupante com uma base de dados de documentos que não impõe qualquer esquema na escrita.
É importante identificar os nossos padrões de acesso desde o início porque esta lista de pedidos vai ser o nosso conjunto de testes. Sempre que iteramos sobre o nosso modelo de dados, analisamos cada um dos pedidos e verificamos o seu desempenho e escalabilidade. Calculamos as unidades de pedido (RU) consumidas em cada modelo e otimizámo-las. Todos estes modelos usam a política de indexação por defeito e pode sobrepô-la indexando propriedades específicas, o que pode melhorar ainda mais o consumo e a latência das unidades de solicitação.
V1: Uma primeira versão
Começamos com dois recipientes: users e posts.
Contêiner de utilizadores
Este contentor armazena apenas os itens do utilizador:
{
"id": "<user-id>",
"username": "<username>"
}
Particionamos este contentor por id, o que significa que cada partição lógica dentro desse contentor contém apenas um item.
Contêiner de publicações
Este contêiner aloja entidades como publicações, comentários e curtidas.
{
"id": "<post-id>",
"type": "post",
"postId": "<post-id>",
"userId": "<post-author-id>",
"title": "<post-title>",
"content": "<post-content>",
"creationDate": "<post-creation-date>"
}
{
"id": "<comment-id>",
"type": "comment",
"postId": "<post-id>",
"userId": "<comment-author-id>",
"content": "<comment-content>",
"creationDate": "<comment-creation-date>"
}
{
"id": "<like-id>",
"type": "like",
"postId": "<post-id>",
"userId": "<liker-id>",
"creationDate": "<like-creation-date>"
}
Particionamos este contentor por postId, o que significa que cada partição lógica dentro desse contentor contém um post, todos os comentários desse post e todos os gostos daquele post.
Introduzimos uma type propriedade nos itens armazenados neste contentor para distinguir entre os três tipos de entidades que este contentor alberga.
Além disso, optámos por referenciar dados relacionados em vez de os incorporar porque:
- Não há limite máximo para o número de publicações que um utilizador pode criar.
- As publicações podem ser arbitrariamente longas.
- Não há limite máximo para o número de comentários e gostos que uma publicação pode ter.
- Queremos poder adicionar um comentário ou um gosto a uma publicação sem ter de atualizar a publicação em si.
Para saber mais sobre estes conceitos, consulte Modelação de dados no Azure Cosmos DB.
Quão bem se comporta o nosso modelo?
Agora é altura de avaliar o desempenho e a escalabilidade da nossa primeira versão. Para cada um dos pedidos previamente identificados, medimos a sua latência e quantas unidades de pedido consome. Esta medição é feita contra um conjunto de dados fictício contendo 100.000 utilizadores, com 5 a 50 publicações por utilizador, e até 25 comentários e 100 gostos por publicação.
[C1] Criar ou editar um utilizador
Este pedido é simples de implementar, pois simplesmente criamos ou atualizamos um item no users contentor. Os pedidos distribuem-se uniformemente por todas as partições graças à id chave de partição.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
7 ms |
5.71 RU |
✅ |
[Q1] Recuperar um utilizador
A recuperação de um utilizador é feita lendo o item correspondente do users contentor.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
2 ms |
1 RU |
✅ |
[C2] Crie ou edite uma publicação
De forma semelhante ao [C1], só temos de escrever no posts container.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
9 ms |
8.76 RU |
✅ |
[Q2] Recuperar uma publicação
Começamos por recuperar o documento correspondente do posts contentor. Mas isso não chega, conforme a nossa especificação, também temos de agregar o nome de utilizador do autor da publicação, as contagens de comentários e as contagens de gostos da publicação. As agregações listadas requerem que sejam emitidas mais três consultas SQL.
Cada uma das consultas filtra a chave de partição do seu respetivo contentor, que é exatamente o que queremos para maximizar o desempenho e a escalabilidade. Mas acabamos por ter de realizar quatro operações para devolver um único post, por isso vamos melhorar isso numa próxima iteração.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
9 ms |
19.54 RU |
⚠ |
[P3] Liste as publicações de um utilizador em formato curto
Primeiro, temos de recuperar as publicações desejadas com uma consulta SQL que recupere as publicações correspondentes a esse utilizador em particular. Mas também temos de realizar mais consultas para agregar o nome de utilizador do autor e a contagem de comentários e gostos.
Esta implementação apresenta muitas desvantagens:
- As consultas que agregam as contagens de comentários e curtidas devem ser realizadas para cada publicação devolvida pela primeira consulta.
- A consulta principal não filtra na chave de partição do
postscontentor, causando um fan-out e uma varredura de partição através do contentor.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
130 ms |
619.41 RU |
⚠ |
[C3] Crie um comentário
Um comentário é criado escrevendo o item correspondente no posts contentor.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
7 ms |
8.57 RU |
✅ |
[Q4] Liste os comentários de uma publicação
Começamos com uma consulta que recolhe todos os comentários dessa publicação e, mais uma vez, precisamos também de agregar os nomes de utilizador separadamente para cada comentário.
Embora a consulta principal filtre na chave de partição do contentor, agregar os nomes de utilizador separadamente penaliza o desempenho global. Melhoramos isso mais tarde.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
23 ms |
27.72 RU |
⚠ |
[C4] Gostar de um post
Tal como [C3], criamos o item correspondente no posts contentor.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
6 ms |
7.05 RU |
✅ |
[Q5] Liste likes de uma publicação
Tal como [Q4], consultamos as reacções dessa publicação e depois agregamos os nomes de utilizador.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
59 ms |
58.92 RU |
⚠ |
[Q6] Lista os x posts mais recentes criados em formato curto (feed)
Recolhemos as publicações mais recentes consultando o posts contentor ordenado por data de criação decrescente, depois agregamos nomes de utilizador e contagens de comentários e gostos de cada publicação.
Mais uma vez, a nossa consulta inicial não filtra na chave de partição do contentor posts, o que desencadeia um dispendioso espalhamento. Esta é ainda pior porque pretendemos alcançar um conjunto de resultados maior e ordenamos os resultados utilizando uma cláusula ORDER BY, o que torna-o mais dispendioso em termos de unidades de pedido.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
306 ms |
2063.54 RU |
⚠ |
Reflita sobre o desempenho da V1
Analisando os problemas de desempenho que enfrentámos na secção anterior, podemos identificar duas classes principais de problemas:
- Alguns pedidos exigem múltiplas consultas para recolher todos os dados que precisamos de devolver.
- Algumas consultas não filtram na chave de partição dos contentores que visam, levando a uma dispersão que prejudica a nossa escalabilidade.
Vamos resolver cada um desses problemas, começando pelo primeiro.
V2: Introduzir a desnormalização para otimizar consultas de leitura
A razão pela qual temos de emitir mais pedidos em alguns casos é porque os resultados do pedido inicial não contêm todos os dados que precisamos de devolver. Desnormalizar dados resolve este tipo de problema em todo o nosso conjunto de dados quando se trabalha com um armazenamento de dados não relacional como o Azure Cosmos DB.
No nosso exemplo, modificamos os itens da publicação para adicionar o nome de utilizador do autor da publicação, a contagem de comentários e a contagem de gostos:
{
"id": "<post-id>",
"type": "post",
"postId": "<post-id>",
"userId": "<post-author-id>",
"userUsername": "<post-author-username>",
"title": "<post-title>",
"content": "<post-content>",
"commentCount": <count-of-comments>,
"likeCount": <count-of-likes>,
"creationDate": "<post-creation-date>"
}
Também modificamos os itens de comentários e gostos para adicionar o nome de utilizador do utilizador que os criou:
{
"id": "<comment-id>",
"type": "comment",
"postId": "<post-id>",
"userId": "<comment-author-id>",
"userUsername": "<comment-author-username>",
"content": "<comment-content>",
"creationDate": "<comment-creation-date>"
}
{
"id": "<like-id>",
"type": "like",
"postId": "<post-id>",
"userId": "<liker-id>",
"userUsername": "<liker-username>",
"creationDate": "<like-creation-date>"
}
Desnormalizar a contagem de comentários e gostos
O que queremos conseguir é que, sempre que adicionamos um comentário ou um gosto, também incrementemos o commentCount ou o likeCount no post correspondente. À medida que o nosso postId contentor é particionado, o novo item (comentário ou like) e a sua publicação correspondente ficam na mesma partição lógica. Como resultado, podemos usar um procedimento armazenado para realizar essa operação.
Quando cria um comentário ([C3]), em vez de simplesmente adicionar um novo item ao posts contentor, chamamos o seguinte procedimento armazenado nesse contentor:
function createComment(postId, comment) {
var collection = getContext().getCollection();
collection.readDocument(
`${collection.getAltLink()}/docs/${postId}`,
function (err, post) {
if (err) throw err;
post.commentCount++;
collection.replaceDocument(
post._self,
post,
function (err) {
if (err) throw err;
comment.postId = postId;
collection.createDocument(
collection.getSelfLink(),
comment
);
}
);
})
}
Este procedimento armazenado toma o ID da publicação e o corpo do novo comentário como parâmetros, depois:
- recupera a publicação.
- incrementa o
commentCount. - substitui o poste.
- acrescenta o novo comentário.
Como os procedimentos armazenados são executados como transações atómicas, o valor de commentCount e o número real de comentários mantêm-se sempre sincronizados.
Obviamente, chamamos um procedimento armazenado semelhante quando se adicionam novos likes para aumentar o likeCount.
Desnormalizar nomes de utilizador
Os nomes de utilizador exigem uma abordagem diferente, pois os utilizadores não só ficam em partições diferentes, mas também num contentor diferente. Quando temos de desnormalizar dados entre partições e contentores, podemos usar o feed de alterações do contentor de origem.
No nosso exemplo, usamos o feed de alterações do users contentor para reagir sempre que os utilizadores atualizam os seus nomes de utilizador. Quando isso acontece, propagamos a alteração chamando outro procedimento armazenado no posts contentor:
function updateUsernames(userId, username) {
var collection = getContext().getCollection();
collection.queryDocuments(
collection.getSelfLink(),
`SELECT * FROM p WHERE p.userId = '${userId}'`,
function (err, results) {
if (err) throw err;
for (var i in results) {
var doc = results[i];
doc.userUsername = username;
collection.upsertDocument(
collection.getSelfLink(),
doc);
}
});
}
Este procedimento armazenado recebe o ID do utilizador e o novo nome de utilizador como parâmetros, em seguida:
- recupera todos os itens que correspondem ao
userId(que podem ser publicações, comentários ou gostos). - Para cada um desses itens:
- substitui o
userUsername. - substitui o item.
- substitui o
Importante
Esta operação é dispendiosa porque requer que este procedimento armazenado seja executado em todas as partições do posts contentor. Assumimos que a maioria dos utilizadores escolhe um nome de utilizador adequado durante o registo e nunca o altera, por isso esta atualização é muito raramente.
Quais são as melhorias de desempenho da V2?
Vamos falar sobre alguns dos ganhos de desempenho do V2.
[Q2] Recuperar uma publicação
Agora que a nossa desnormalização está implementada, apenas precisamos de buscar um único item para tratar dessa requisição.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
2 ms |
1 RU |
✅ |
[Q4] Liste os comentários de uma publicação
Mais uma vez, podemos poupar os pedidos adicionais que recuperaram os nomes de utilizador, resultando numa única consulta que filtra a chave de partição.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
4 ms |
7.72 RU |
✅ |
[Q5] Liste likes de uma publicação
Exatamente a mesma situação ao listar os likes.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
4 ms |
8.92 RU |
✅ |
V3: Garantir que todos os pedidos são escaláveis
Ainda há dois pedidos que não otimizámos totalmente quando analisamos as melhorias globais de desempenho. Estes pedidos são [Q3] e [Q6]. São as requisições que envolvem consultas que não filtrem pela chave de partição dos contentores aos quais têm como alvo.
[P3] Liste as publicações de um utilizador em formato curto
Este pedido já beneficia das melhorias introduzidas na V2, que economiza mais consultas de dados.
Mas a consulta restante continua sem filtrar na chave de partição do posts contentor.
A forma de pensar nesta situação é simples:
- Este pedido tem de filtrar em
userIdporque queremos obter todas as publicações de um utilizador em particular. - Não tem um desempenho adequado porque é executado contra o
postscontentor, que não está particionado com ouserId. - Afirmando o óbvio, resolveríamos o nosso problema de desempenho executando este pedido contra um contentor particionado com
userId. - Acontece que já temos um contentor assim: o
userscontentor!
Por isso, introduzimos um segundo nível de desnormalização ao duplicar publicações inteiras no users contentor. Ao fazer isso, obtemos efetivamente uma cópia das nossas publicações, só que particionadas numa dimensão diferente, tornando-as muito mais eficientes de recuperar pelo seu userId.
O users recipiente contém agora dois tipos de itens:
{
"id": "<user-id>",
"type": "user",
"userId": "<user-id>",
"username": "<username>"
}
{
"id": "<post-id>",
"type": "post",
"postId": "<post-id>",
"userId": "<post-author-id>",
"userUsername": "<post-author-username>",
"title": "<post-title>",
"content": "<post-content>",
"commentCount": <count-of-comments>,
"likeCount": <count-of-likes>,
"creationDate": "<post-creation-date>"
}
Neste exemplo:
- Introduzimos um campo
typeno item de utilizador para distinguir utilizadores de publicações. - Também adicionámos um
userIdcampo no item do utilizador, que é redundante com oidcampo mas é obrigatório porque ouserscontentor agora está particionado comuserId(e nãoidcomo antes).
Para alcançar esta desnormalização, voltamos a usar o feed de alteração. Desta vez, reagimos ao feed de alterações do posts contentor para enviar qualquer publicação nova ou atualizada para o users contentor. Uma vez que listar publicações não exige devolver o conteúdo completo, podemos truncá-las durante o processo.
Agora podemos encaminhar a nossa consulta para o users contentor, filtrando pela chave de partição do contentor.
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
4 ms |
6.46 RU |
✅ |
[Q6] Lista os x posts mais recentes criados em formato curto (feed)
Temos de lidar com uma situação semelhante aqui: mesmo após minimizar as consultas que foram deixadas desnecessárias pela desnormalização introduzida no V2, a consulta restante não filtra na chave de partição do contentor:
Seguindo a mesma abordagem, maximizar o desempenho e a escalabilidade deste pedido requer que ele atinja apenas uma partição. Só é possível acessar uma única partição porque só temos de retornar um número limitado de itens. Para preencher a página inicial da nossa plataforma de blogues, basta obter as 100 publicações mais recentes, sem necessidade de paginar todo o conjunto de dados.
Para otimizar este último pedido, introduzimos um terceiro contentor no nosso design, inteiramente dedicado a servir este pedido. Nós desnormalizamos os nossos posts para esse novo feed container:
{
"id": "<post-id>",
"type": "post",
"postId": "<post-id>",
"userId": "<post-author-id>",
"userUsername": "<post-author-username>",
"title": "<post-title>",
"content": "<post-content>",
"commentCount": <count-of-comments>,
"likeCount": <count-of-likes>,
"creationDate": "<post-creation-date>"
}
Nos nossos elementos, o type campo particiona este contentor, que está sempre post. Fazer isso garante que todos os itens deste contentor ficam na mesma partição.
Para conseguir a desnormalização, só temos de nos ligar ao pipeline de alimentação de alterações que introduzimos anteriormente para despachar os posts para esse novo contentor. Uma coisa importante a ter em conta é que temos de garantir que armazenamos apenas as 100 publicações mais recentes; caso contrário, o conteúdo do contentor pode crescer para além do tamanho máximo de uma partição. Esta limitação pode ser implementada chamando um post-trigger sempre que um documento é adicionado ao contentor:
Aqui está o script do pós-trigger para truncar a coleção:
function truncateFeed() {
const maxDocs = 100;
var context = getContext();
var collection = context.getCollection();
collection.queryDocuments(
collection.getSelfLink(),
"SELECT VALUE COUNT(1) FROM f",
function (err, results) {
if (err) throw err;
processCountResults(results);
});
function processCountResults(results) {
// + 1 because the query didn't count the newly inserted doc
if ((results[0] + 1) > maxDocs) {
var docsToRemove = results[0] + 1 - maxDocs;
collection.queryDocuments(
collection.getSelfLink(),
`SELECT TOP ${docsToRemove} * FROM f ORDER BY f.creationDate`,
function (err, results) {
if (err) throw err;
processDocsToRemove(results, 0);
});
}
}
function processDocsToRemove(results, index) {
var doc = results[index];
if (doc) {
collection.deleteDocument(
doc._self,
function (err) {
if (err) throw err;
processDocsToRemove(results, index + 1);
});
}
}
}
O passo final é redirecionar a nossa consulta para o nosso novo feed contentor:
| Latency | Unidades de Solicitação | Desempenho |
|---|---|---|
9 ms |
16.97 RU |
✅ |
Conclusion
Vamos analisar as melhorias globais de desempenho e escalabilidade que introduzimos em relação às diferentes versões do nosso design.
| V1 | V2 | V3 | |
|---|---|---|---|
| [C1] |
7 ms / 5.71 RU |
7 ms / 5.71 RU |
7 ms / 5.71 RU |
| [Q1] |
2 ms / 1 RU |
2 ms / 1 RU |
2 ms / 1 RU |
| [C2] |
9 ms / 8.76 RU |
9 ms / 8.76 RU |
9 ms / 8.76 RU |
| [Q2] |
9 ms / 19.54 RU |
2 ms / 1 RU |
2 ms / 1 RU |
| [P3] |
130 ms / 619.41 RU |
28 ms / 201.54 RU |
4 ms / 6.46 RU |
| [C3] |
7 ms / 8.57 RU |
7 ms / 15.27 RU |
7 ms / 15.27 RU |
| [Q4] |
23 ms / 27.72 RU |
4 ms / 7.72 RU |
4 ms / 7.72 RU |
| [C4] |
6 ms / 7.05 RU |
7 ms / 14.67 RU |
7 ms / 14.67 RU |
| [Q5] |
59 ms / 58.92 RU |
4 ms / 8.92 RU |
4 ms / 8.92 RU |
| [Q6] |
306 ms / 2063.54 RU |
83 ms / 532.33 RU |
9 ms / 16.97 RU |
Otimizámos um cenário com muita leitura
Pode notar que concentrámos os nossos esforços em melhorar o desempenho dos pedidos de leitura (consultas) em detrimento dos pedidos de escrita (comandos). Em muitos casos, as operações de escrita agora desencadeiam desnormalizações subsequentes através de fluxos de alterações, o que as torna mais dispendiosas computacionalmente e mais demoradas a concretizar-se.
Justificamos este foco no desempenho de leitura pelo facto de uma plataforma de blogues, como a maioria das aplicações sociais, ser muito rica em leituras. Uma carga de trabalho pesada em leitura indica que o número de pedidos de leitura que precisa atender é geralmente ordens de magnitude maiores que o número de pedidos de escrita. Por isso, faz sentido tornar os pedidos de escrita mais caros de executar para que os pedidos de leitura sejam mais baratos e com melhor desempenho.
Se olharmos para a otimização mais extrema que fizemos, [Q6] passou de 2000+ RUs para apenas 17 RUs; conseguimos isso ao desnormalizar publicações a um custo de cerca de 10 RUs por item. Como atenderíamos muito mais pedidos de feed do que criação ou atualizações de publicações, o custo desta desnormalização é negligenciável tendo em conta a poupança global.
A desnormalização pode ser aplicada de forma incremental
As melhorias de escalabilidade que explorámos neste artigo envolvem a desnormalização e duplicação de dados ao longo do conjunto de dados. Deve notar-se que estas otimizações não precisam de ser implementadas logo no primeiro dia. Consultas que filtram em chaves de partição têm melhor desempenho em grande escala, mas consultas entre partições podem ser aceitáveis se forem chamadas raramente ou sobre um conjunto de dados limitado. Se estiveres apenas a construir um protótipo, ou a lançar um produto com uma base de utilizadores pequena e controlada, provavelmente podes deixar essas melhorias para mais tarde. O importante então é monitorizar o desempenho do seu modelo para poder decidir se e quando é altura de os trazer.
O feed de alterações que usamos para distribuir atualizações para outros contentores armazena todas essas atualizações de forma persistente. Esta persistência torna possível solicitar todas as atualizações desde a criação do contentor e desnormalizar vistas de bootstrap como uma operação de recuperação pontual, mesmo que o seu sistema já tenha muitos dados.
Próximos passos
Após esta introdução à modelação prática de dados e particionamento, poderá querer consultar os seguintes artigos para rever os conceitos: