Estatísticas de divisão

Esta página descreve como detetar e depurar pontos críticos na sua base de dados. Pode aceder a estatísticas sobre pontos críticos em divisões com o GoogleSQL e o PostgreSQL.

O Spanner armazena os seus dados como um espaço de chaves contíguo, ordenado pelas chaves primárias das suas tabelas e índices. Uma divisão é um intervalo de linhas de um conjunto de tabelas ou um índice. O início da divisão chama-se início da divisão. O limite de divisão define o fim da divisão. A divisão inclui o início da divisão, mas não o limite da divisão.

No Spanner, os hotspots são situações em que são enviados demasiados pedidos para o mesmo servidor, o que satura os recursos do servidor e causa potencialmente latências elevadas. As divisões afetadas por pontos críticos são conhecidas como divisões quentes ou mornas.

A estatística de hotspot de uma divisão (identificada no sistema como CPU_USAGE_SCORE) é uma medição da carga numa divisão que está limitada pelos recursos disponíveis no servidor. Esta medição é apresentada em percentagem. Se mais de 50% da carga numa divisão estiver restrita pelos recursos disponíveis, a divisão é considerada quente. Se 100% do carregamento numa divisão estiver restrito, a divisão é considerada ativa.

O Spanner usa a divisão baseada na carga para distribuir uniformemente a carga de dados pelos servidores da instância. As divisões quentes e muito quentes podem ser movidas entre servidores para equilibrar a carga ou podem ser divididas em divisões mais pequenas. No entanto, o Spanner pode não conseguir equilibrar a carga, mesmo após várias tentativas de divisão, devido a padrões de anti-design na aplicação. Por conseguinte, os pontos críticos persistentes que duram, pelo menos, 10 minutos podem precisar de resolução de problemas adicional e potenciais alterações à aplicação.

As estatísticas de divisão frequente do Spanner ajudam a identificar as divisões onde ocorrem pontos críticos. Em seguida, pode fazer alterações à sua aplicação ou esquema, conforme necessário. Pode obter estas estatísticas das tabelas do sistema SPANNER_SYS.SPLIT_STATS_TOP_MINUTE através de declarações SQL.

Aceda às estatísticas de divisão frequente

O Spanner fornece as estatísticas de divisão frequente no esquema SPANNER_SYS. Os dados do SPANNER_SYS só estão disponíveis através das interfaces GoogleSQL e PostgreSQL. Pode usar as seguintes formas para aceder a estes dados:

Os seguintes métodos de leitura única fornecidos pelo Spanner não suportam o SPANNER_SYS:

  • Executar uma leitura forte a partir de uma única linha ou de várias linhas numa tabela.
  • Executar uma leitura desatualizada de uma única linha ou várias linhas numa tabela.
  • Leitura de uma única linha ou várias linhas num índice secundário.

Estatísticas de tempos parciais rápidos

Use a tabela seguinte para acompanhar as divisões frequentes:

  • SPANNER_SYS.SPLIT_STATS_TOP_MINUTE: mostra divisões que estão em alta durante intervalos de 1 minuto.

Estas tabelas têm as seguintes propriedades:

  • Cada tabela contém dados para intervalos de tempo não sobrepostos da duração especificada no nome da tabela.
  • Os intervalos baseiam-se nas horas do relógio:

    • Os intervalos de 1 minuto terminam no minuto.
  • Após cada intervalo, o Spanner recolhe dados de todos os servidores e, em seguida, disponibiliza os dados nas tabelas SPANNER_SYS pouco depois.

    Por exemplo, às 11:59:30, os intervalos mais recentes disponíveis para consultas SQL são:

    • 1 minuto: 11:58:00-11:58:59
  • O Spanner agrupa as estatísticas por divisões.

  • Cada linha contém uma percentagem que indica o quão quente ou morna é uma divisão, para cada divisão para a qual o Spanner captura estatísticas durante o intervalo especificado.

  • Se menos de 50% da carga num intervalo estiver restrita pelos recursos disponíveis, o Spanner não captura a estatística. Se o Spanner não conseguir armazenar todas as divisões frequentes durante o intervalo, o sistema dá prioridade às divisões com a percentagem CPU_USAGE_SCORE mais elevada durante o intervalo especificado. Se não forem devolvidas divisões, é uma indicação da ausência de pontos críticos.

Esquema da tabela

A tabela seguinte mostra o esquema da tabela para as seguintes estatísticas:

  • SPANNER_SYS.SPLIT_STATS_TOP_MINUTE
Nome da coluna Tipo Descrição
INTERVAL_END TIMESTAMP Fim do intervalo de tempo durante o qual a divisão estava quente
SPLIT_START STRING A chave inicial do intervalo de linhas na divisão. A divisão inicial também pode ser <begin>, o que indica o início do espaço da chave
SPLIT_LIMIT STRING A chave de limite para o intervalo de linhas na divisão. A tecla: limit também pode ser <end>, o que indica o fim do espaço das teclas|
CPU_USAGE_SCORE INT64 A percentagem CPU_USAGE_SCORE das divisões. Uma percentagem de 50% indica a presença de divisões quentes ou frias | splits |CPU_USAGE_SCORE
AFFECTED_TABLES STRING ARRAY As tabelas cujas linhas podem estar na divisão

Teclas de início e limite de divisão

Uma divisão é um intervalo de linhas contíguo de uma base de dados e é definida pelas chaves start e limit. Uma divisão pode ser uma única linha, um intervalo de linhas estreito ou um intervalo de linhas amplo, e a divisão pode incluir várias tabelas ou índices.

As colunas SPLIT_START e SPLIT_LIMIT identificam as chaves principais de uma divisão morna ou ativa.

Esquema de exemplo

O esquema seguinte é uma tabela de exemplo para os tópicos desta página.

GoogleSQL

CREATE TABLE Users (
  UserId INT64 NOT NULL,
  FirstName STRING(MAX),
  LastName STRING(MAX),
) PRIMARY KEY(UserId);

CREATE INDEX UsersByFirstName ON Users(FirstName DESC);

CREATE TABLE Threads (
  UserId INT64 NOT NULL,
  ThreadId INT64 NOT NULL,
  Starred BOOL,
) PRIMARY KEY(UserId, ThreadId),
  INTERLEAVE IN PARENT Users ON DELETE CASCADE;

CREATE TABLE Messages (
  UserId INT64 NOT NULL,
  ThreadId INT64 NOT NULL,
  MessageId INT64 NOT NULL,
  Subject STRING(MAX),
  Body STRING(MAX),
) PRIMARY KEY(UserId, ThreadId, MessageId),
  INTERLEAVE IN PARENT Threads ON DELETE CASCADE;

CREATE INDEX MessagesIdx ON Messages(UserId, ThreadId, Subject),
INTERLEAVE IN Threads;

PostgreSQL

CREATE TABLE users
(
   userid    BIGINT NOT NULL PRIMARY KEY,-- INT64 to BIGINT
   firstname VARCHAR(max),-- STRING(MAX) to VARCHAR(MAX)
   lastname  VARCHAR(max)
);

CREATE INDEX usersbyfirstname
  ON users(firstname DESC);

CREATE TABLE threads
  (
    userid   BIGINT NOT NULL,
    threadid BIGINT NOT NULL,
    starred  BOOLEAN, -- BOOL to BOOLEAN
    PRIMARY KEY (userid, threadid),
    CONSTRAINT fk_threads_user FOREIGN KEY (userid) REFERENCES users(userid) ON
    DELETE CASCADE -- Interleave to Foreign Key constraint
  );

CREATE TABLE messages
  (
    userid    BIGINT NOT NULL,
    threadid  BIGINT NOT NULL,
    messageid BIGINT NOT NULL PRIMARY KEY,
    subject   VARCHAR(max),
    body      VARCHAR(max),
    CONSTRAINT fk_messages_thread FOREIGN KEY (userid, threadid) REFERENCES
    threads(userid, threadid) ON DELETE CASCADE
  -- Interleave to Foreign Key constraint
  );

CREATE INDEX messagesidx ON messages(userid, threadid, subject), REFERENCES
threads(userid, threadid);

Imagine que o seu espaço de chaves tem o seguinte aspeto:

CHAVE PRINCIPAL
<begin>
Users()
Threads()
Users(2)
Users(3)
Threads(3)
Threads(3,"a")
Messages(3,"a",1)
Messages(3,"a",2)
Threads(3, "aa")
Users(9)
Users(10)
Threads(10)
UsersByFirstName("abc")
UsersByFirstName("abcd")
<end>

Exemplo de divisões

O exemplo seguinte mostra algumas divisões para ajudar a compreender o aspeto das divisões.

SPLIT_START e SPLIT_LIMIT podem indicar a linha de uma tabela ou um índice, ou podem ser <begin> e <end>, que representam os limites do espaço de chaves da base de dados. Os elementos SPLIT_START e SPLIT_LIMIT também podem conter chaves truncadas, que são chaves que precedem qualquer chave completa na tabela. Por exemplo, Threads(10) é um prefixo para qualquer linha Threads intercalada em Users(10).

SPLIT_START SPLIT_LIMIT AFFECTED_TABLES EXPLICAÇÃO
Users(3) Users(10) UsersByFirstName, Users, Threads, Messages e MessagesIdx A divisão começa na linha com UserId=3 e termina na linha antes da linha com UserId = 10. A divisão contém as linhas da tabela Users e todas as linhas das tabelas intercaladas para UserId=3 a 10.
Messages(3,"a",1) Threads(3,"aa") Threads, Messages, MessagesIdx A divisão começa na linha com UserId=3, ThreadId="a" e MessageId=1 e termina na linha anterior à linha com a chave de UserId=3 e ThreadsId = "aa". A divisão contém todas as tabelas entre Messages(3,"a",1) e Threads(3,"aa"). Uma vez que os elementos split_start e split_limit estão intercalados na mesma linha da tabela de nível superior, a divisão contém as linhas das tabelas intercaladas entre o início e o limite. Consulte o artigo schemas-overview para compreender como as tabelas intercaladas estão localizadas em conjunto.
Messages(3,"a",1) <end> UsersByFirstName, Users, Threads, Messages e MessagesIdx A divisão começa na tabela de mensagens na linha com a chave UserId=3, ThreadId="a" e MessageId=1. A divisão aloja todas as linhas de split_start a <end>, o fim do espaço de chaves da base de dados. Todas as linhas das tabelas que seguem o split_start, como Users(4), estão incluídas na divisão.
<begin> Users(9) UsersByFirstName, Users, Threads, Messages e MessagesIdx A divisão começa em <begin>, o início do espaço de chaves da base de dados, e termina na linha anterior à linha Users com UserId=9. Assim, a divisão tem todas as linhas da tabela anteriores a Users e todas as linhas da tabela Users anteriores a UserId=9, bem como as linhas das respetivas tabelas intercaladas.
Messages(3,"a",1) Threads(10) UsersByFirstName, Users, Threads, Messages e MessagesIdx A divisão começa na linha Messages(3,"a", 1) intercalada em Users(3) e termina na linha anterior a Threads(10). Threads(10) é uma chave dividida truncada que é um prefixo de qualquer chave da tabela Threads intercalada em Users(10).
Users() <end> UsersByFirstName, Users, Threads, Messages e MessagesIdx A divisão começa na chave de divisão truncada de Users(), que precede qualquer chave completa da tabela Users. A divisão estende-se até ao final do espaço de chaves possível na base de dados. Assim, as affected_tables abrangem a tabela Users, as respetivas tabelas e índices intercalados, e todas as tabelas que possam aparecer após os utilizadores.
Threads(10) UsersByFirstName("abc") UsersByFirstName, Users, Threads, Messages e MessagesIdx A divisão começa na linha Threads com UserId = 10 e termina no índice UsersByFirstName na chave anterior a "abc".

Exemplos de consultas para encontrar divisões quentes

O exemplo seguinte mostra uma declaração SQL que pode usar para obter as estatísticas de divisão dinâmica. Pode executar estas declarações SQL através das bibliotecas de cliente, do gcloud ou da Google Cloud consola.

GoogleSQL

SELECT t.split_start,
       t.split_limit,
       t.cpu_usage_score,
       t.affected_tables,
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.interval_end =
  (SELECT MAX(interval_end)
  FROM    SPANNER_SYS.SPLIT_STATS_TOP_MINUTE)
ORDER BY  t.cpu_usage_score DESC;

PostgreSQL

SELECT t.split_start,
       t.split_limit,
       t.cpu_usage_score,
       t.affected_tables
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.interval_end = (
  SELECT MAX(interval_end)
  FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE
)
ORDER BY t.cpu_usage_score DESC;

O resultado da consulta tem o seguinte aspeto:

SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE AFFECTED_TABLES
Users(13) Users(76) 82 Messages,Users,Threads
Users(101) Users(102) 90 Messages,Users,Threads
Threads(10, "a") Threads(10, "aa") 100 Messages,Threads
Messages(631, "abc", 1) Messages(631, "abc", 3) 100 Messages
Threads(12, "zebra") Users(14) 76 Messages,Users,Threads
Users(620) <end> 100 Messages,Users,Threads

Retenção de dados para as estatísticas de divisão frequente

No mínimo, o Spanner mantém os dados de cada tabela durante o seguinte período:

  • SPANNER_SYS.SPLIT_STATS_TOP_MINUTE: intervalos que abrangem as 6 horas anteriores.

Resolva problemas de pontos ativos através de estatísticas de divisão de calor

Esta secção descreve como detetar e resolver problemas de pontos críticos.

Selecione um período para investigar

Verifique as métricas de latência da sua base de dados do Spanner para encontrar o período em que a sua aplicação registou uma latência e uma utilização da CPU elevadas. Por exemplo, pode mostrar que um problema começou por volta das 22:50 de 18 de maio de 2024.

Encontre pontos de acesso persistentes

Uma vez que o Spanner equilibra a carga com a divisão baseada na carga, recomendamos que investigue se a concentração de acessos continuou durante mais de 10 minutos. Pode fazê-lo consultando a tabela SPANNER_SYS.SPLIT_STATS_TOP_MINUTE, conforme mostrado no exemplo seguinte:

GoogleSQL

SELECT Count(DISTINCT t.interval_end)
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.cpu_usage_score >= 50
  AND  t.interval_end >= "interval_end_date_time"
  AND  t.interval_end <= "interval_end_date_time";

Substitua interval_end_date_time pela data e hora do intervalo, usando o formato 2024-05-18T17:40:00Z.

PostgreSQL

SELECT COUNT(DISTINCT t.interval_end)
FROM   SPLIT_STATS_TOP_MINUTE t
WHERE  t.cpu_usage_score >= 50
  AND  t.interval_end >= 'interval_end_date_time'::timestamptz
  AND  t.interval_end <= 'interval_end_date_time'::timestamptz;

Substitua interval_end_date_time pela data e hora do intervalo, usando o formato 2024-05-18T17:40:00Z.

Se o resultado da consulta anterior for igual a 10, significa que a sua base de dados está a ter problemas de hotspotting que podem precisar de mais depuração.

Encontre as divisões com o nível CPU_USAGE_SCORE mais elevado

Para este exemplo, executamos o seguinte SQL para encontrar os intervalos de linhas com o nível CPU_USAGE_SCORE mais elevado:

GoogleSQL

SELECT t.split_start,
       t.split_limit,
       t.affected_tables,
       t.cpu_usage_score
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.cpu_usage_score >= 50
  AND  t.interval_end = "interval_end_date_time";

Substitua interval_end_date_time pela data e hora do intervalo, usando o formato 2024-05-18T17:40:00Z.

PostgreSQL

SELECT t.split_start,
       t.split_limit,
       t.affected_tables,
       t.cpu_usage_score
FROM   SPLIT_STATS_TOP_MINUTE t
WHERE  t.cpu_usage_score = 100
  AND  t.interval_end = 'interval_end_date_time'::timestamptz;

Substitua interval_end_date_time pela data e hora do intervalo, usando o formato 2024-05-18T17:40:00Z.

O SQL anterior produz o seguinte:

SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE AFFECTED_TABLES
Users(180) <end> 85 Messages,Users,Threads
Users(24) Users(76) 76 Messages,Users,Threads

A partir desta tabela de resultados, podemos ver que ocorreram pontos críticos em duas divisões. A divisão baseada no carregamento do Spanner pode tentar resolver os pontos críticos nestas divisões. No entanto, pode não conseguir fazê-lo se existirem padrões problemáticos no esquema ou na carga de trabalho. Para detetar se existem divisões que requerem a sua intervenção, recomendamos que monitorize as divisões durante, pelo menos, 10 minutos. Por exemplo, o seguinte SQL acompanha a primeira divisão nos últimos dez minutos.

GoogleSQL

SELECT t.interval_end,
       t.split_start,
       t.split_limit,
       t.cpu_usage_score
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.split_start = "users(180)"
  AND  t.split_limit = "<end>"
  AND  t.interval_end >= "interval_end_date_time"
  AND  t.interval_end <= "interval_end_date_time";

Substitua interval_end_date_time pela data e hora do intervalo, usando o formato 2024-05-18T17:40:00Z.

PostgreSQL

SELECT t.interval_end,
       t.split_start,
       t.split_limit,
       t.cpu_usage_score
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.split_start = 'users(180)'
  AND  t.split_limit = ''
  AND  t.interval_end >= 'interval_end_date_time'::timestamptz
  AND  t.interval_end <= 'interval_end_date_time'::timestamptz;

Substitua interval_end_date_time pela data e hora do intervalo, usando o formato 2024-05-18T17:40:00Z.

O SQL anterior produz o seguinte:

INTERVAL_END SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE
2024-05-18T17:46:00Z Users(180) <end> 85
2024-05-18T17:47:00Z Users(180) <end> 85
2024-05-18T17:48:00Z Users(180) <end> 85
2024-05-18T17:49:00Z Users(180) <end> 85
2024-05-18T17:50:00Z Users(180) <end> 85

A divisão parece ter estado quente nos últimos minutos. Pode observar a divisão durante mais tempo para determinar que a divisão baseada na carga do Spanner mitiga o ponto crítico. Pode haver casos em que o Spanner não consegue equilibrar a carga mais.

Por exemplo, consulte a tabela SPANNER_SYS.SPLIT_STATS_TOP_MINUTE. Veja os seguintes cenários de exemplo.

GoogleSQL

SELECT t.interval_end,
      t.split_start,
      t.split_limit,
      t.cpu_usage_score
FROM  SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE t.interval_end >= "interval_end_date_time"
      AND t.interval_end <= "interval_end_date_time";

Substitua interval_end_date_time pela data e hora do intervalo, usando o formato 2024-05-18T17:40:00Z.

PostgreSQL

SELECT t.interval_end,
       t.split_start,
       t.split_limit,
       t._cpu_usage
FROM   SPANNER_SYS.SPLIT_STATS_TOP_MINUTE t
WHERE  t.interval_end >= 'interval_end_date_time'::timestamptz
  AND  t.interval_end <= 'interval_end_date_time'::timestamptz;

Substitua interval_end_date_time pela data e hora do intervalo, usando o formato 2024-05-18T17:40:00Z.

Linha ativa única

No exemplo seguinte, parece que Threads(10,"spanner") está numa divisão de linha única que permaneceu quente durante mais de 10 minutos. Isto pode acontecer quando existe um carregamento persistente numa linha popular.

INTERVAL_END SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE
2024-05-16T20:40:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:41:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:42:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:43:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:44:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:45:00Z Threads(10,"spanner") Threads(10,"spanner1") 62
2024-05-16T20:46:00Z Threads(10,"spanner") Threads(10,"spanner1") 80
2024-05-16T20:47:00Z Threads(10,"spanner") Threads(10,"spanner1") 80
2024-05-16T20:48:00Z Threads(10,"spanner") Threads(10,"spanner1") 80
2024-05-16T20:49:00Z Threads(10,"spanner") Threads(10,"spanner1") 100
2024-05-16T20:50:00Z Threads(10,"spanner") Threads(10,"spanner1") 100

O Spanner não consegue equilibrar a carga para esta única chave, uma vez que não pode ser dividida mais.

Zona Wi-Fi em movimento

No exemplo seguinte, a carga move-se através de divisões contíguas ao longo do tempo, passando para uma nova divisão em intervalos de tempo.

INTERVAL_END SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE
2024-05-16T20:40:00Z Threads(1,"a") Threads(1,"aa") 100
2024-05-16T20:41:00Z Threads(1,"aa") Threads(1,"ab") 100
2024-05-16T20:42:00Z Threads(1,"ab") Threads(1,"c") 100
2024-05-16T20:43:00Z Threads(1,"c") Threads(1,"ca") 100

Isto pode ocorrer, por exemplo, devido a uma carga de trabalho que lê ou escreve chaves por ordem crescente monotónica. O Spanner não consegue equilibrar a carga para mitigar os efeitos deste comportamento da aplicação.

Balanceamento de carga normal

O Spanner tenta equilibrar a carga adicionando mais divisões ou movendo as divisões. O exemplo seguinte mostra como pode ser apresentado.

INTERVAL_END SPLIT_START SPLIT_LIMIT CPU_USAGE_SCORE
2024-05-16T20:40:00Z Threads(1000,"zebra") <end> 82
2024-05-16T20:41:00Z Threads(1000,"zebra") <end> 90
2024-05-16T20:42:00Z Threads(1000,"zebra") <end> 100
2024-05-16T20:43:00Z Threads(1000,"zebra") Threads(2000,"spanner") 100
2024-05-16T20:44:00Z Threads(1200,"c") Threads(2000) 92
2024-05-16T20:45:00Z Threads(1500,"c") Threads(1700,"zach") 76
2024-05-16T20:46:00Z Threads(1700) Threads(1700,"c") 76
2024-05-16T20:47:00Z Threads(1700) Threads(1700,"c") 50
2024-05-16T20:48:00Z Threads(1700) Threads(1700,"c") 39

Neste caso, a divisão maior a 2024-05-16T17:40:00Z foi dividida ainda mais numa divisão mais pequena e, como resultado, a estatística CPU_USAGE_SCORE diminuiu. O Spanner pode não criar divisões em linhas individuais. As divisões refletem a carga de trabalho que causa a estatística CPU_USAGE_SCORE elevada.

Se observou uma divisão quente persistente durante mais de 10 minutos, consulte as práticas recomendadas para mitigar os pontos críticos.

Práticas recomendadas para mitigar pontos críticos

Se o equilíbrio de carga não diminuir a latência, o passo seguinte é identificar a causa dos pontos críticos. Depois disso, as opções são reduzir a carga de trabalho de pontos críticos ou otimizar o esquema e a lógica da aplicação para evitar pontos críticos.

Identifique a causa

  • Use as estatísticas de bloqueios e transações para procurar transações com um tempo de espera de bloqueio elevado em que a chave de início do intervalo de linhas se encontra na divisão ativa.

  • Use as estatísticas sobre consultas para procurar consultas que leiam a partir da tabela que contém a divisão frequente e que tenham aumentado recentemente a latência ou uma proporção mais elevada de latência para CPU.

  • Use Consultas ativas mais antigas para procurar consultas que leiam a partir da tabela que contém a divisão frequente e que tenham uma latência superior à esperada.

Alguns casos especiais a ter em atenção:

  • Verifique se o tempo de vida (TTL) foi ativado recentemente. Se existirem muitas divisões de dados antigos, o TTL pode aumentar os níveis durante as eliminações em massa.CPU_USAGE_SCORE Neste caso, o problema deve resolver-se automaticamente assim que as eliminações iniciais forem concluídas.

Otimize a carga de trabalho

O que se segue?