Por que usar TRUNCATE e DROP?

100

No sistema em que trabalho, existem muitos procedimentos armazenados e scripts SQL que fazem uso de tabelas temporárias. Depois de usar essas tabelas, é uma boa prática descartá-las.

Muitos dos meus colegas (quase todos com muito mais experiência do que eu) normalmente fazem isso:

TRUNCATE TABLE #mytemp
DROP TABLE #mytemp

Normalmente, uso um único DROP TABLEem meus scripts.

Existe alguma boa razão para fazer um TRUNCATEimediatamente antes de um DROP?

user606723
fonte

Respostas:

130

Não.

TRUNCATEe DROPsão quase idênticos em comportamento e velocidade, portanto, fazer um TRUNCATEdireito antes de um DROPé simplesmente desnecessário.


Nota: Eu escrevi esta resposta da perspectiva do SQL Server e presumi que ela se aplicaria igualmente ao Sybase. Parece que este não é inteiramente o caso .

Nota: Quando publiquei esta resposta pela primeira vez, havia várias outras respostas altamente classificadas - incluindo a resposta então aceita - que fizeram várias afirmações falsas como: TRUNCATEnão está registrada; TRUNCATEnão pode ser revertido; TRUNCATEé mais rápido que DROP; etc.

Agora que esse segmento foi limpo, as refutações a seguir podem parecer tangenciais à pergunta original. Deixo-os aqui como referência para outros que buscam desmascarar esses mitos.


Existem algumas falsidades populares - difundidas mesmo entre DBAs experientes - que podem ter motivado esse TRUNCATE-then-DROPpadrão. Eles são:

  • Mito : TRUNCATEnão está registrado, portanto, não pode ser revertido.
  • Mito : TRUNCATEé mais rápido que DROP.

Deixe-me refutar essas falsidades. Estou escrevendo essa refutação da perspectiva do SQL Server, mas tudo o que digo aqui deve ser igualmente aplicável ao Sybase.

TRUNCATE é registrado e pode ser revertido.

  • TRUNCATEé uma operação registrada, para que possa ser revertida . Apenas envolva-o em uma transação.

    USE [tempdb];
    SET NOCOUNT ON;
    
    CREATE TABLE truncate_demo (
        whatever    VARCHAR(10)
    );
    
    INSERT INTO truncate_demo (whatever)
    VALUES ('log this');
    
    BEGIN TRANSACTION;
        TRUNCATE TABLE truncate_demo;
    ROLLBACK TRANSACTION;
    
    SELECT *
    FROM truncate_demo;
    
    DROP TABLE truncate_demo;

    Observe, no entanto, que isso não é verdade para o Oracle . Embora registrado e protegido pela funcionalidade de desfazer e refazer do Oracle, TRUNCATEoutras instruções DDL não podem ser revertidas pelo usuário porque os problemas do Oracle implícitos confirmam imediatamente antes e depois de todas as instruções DDL.

  • TRUNCATEé minimamente registrado , em vez de totalmente registrado. O que isso significa? Diga TRUNCATEuma mesa para você . Em vez de colocar cada linha excluída no log de transações, TRUNCATEapenas marca as páginas de dados em que vivem como não alocadas. Por isso é tão rápido. É também por isso que você não pode recuperar as linhas de uma TRUNCATEtabela -ed do log de transações usando um leitor de log. Tudo o que você encontrará são referências às páginas de dados desalocadas.

    Compare isso com DELETE. Se você DELETEtodas as linhas de uma tabela e confirmar a transação, ainda poderá, em teoria, encontrar as linhas excluídas no log de transações e recuperá-las a partir daí. Isso porque DELETEgrava todas as linhas excluídas no log de transações. Para tabelas grandes, isso tornará muito mais lento que TRUNCATE.

DROP é tão rápido quanto TRUNCATE.

  • Como TRUNCATE, DROPé uma operação minimamente registrada. Isso significa que também DROPpode ser revertido. Isso também significa que funciona exatamente da mesma maneira que TRUNCATE. Em vez de excluir linhas individuais, DROPmarca as páginas de dados apropriadas como não alocadas e, adicionalmente, marca os metadados da tabela como excluídos .
  • Porque TRUNCATEe DROPfuncionam exatamente da mesma maneira, eles correm tão rápido quanto o outro. Não há sentido em criar TRUNCATEuma tabela antes de DROPfazê-la. Execute este script de demonstração na sua instância de desenvolvimento, se você não acredita em mim.

    Na minha máquina local com um cache quente, os resultados obtidos são os seguintes:

    table row count: 134,217,728
    
    run#        transaction duration (ms)
          TRUNCATE   TRUNCATE then DROP   DROP
    ==========================================
    01       0               1             4
    02       0              39             1
    03       0               1             1
    04       0               2             1
    05       0               1             1
    06       0              25             1
    07       0               1             1
    08       0               1             1
    09       0               1             1
    10       0              12             1
    ------------------------------------------
    avg      0              8.4           1.3

    Portanto, para uma tabela de 134 milhões de linhas DROPe não TRUNCATEdemore efetivamente. (Em um cache frio, eles levam cerca de 2 a 3 segundos para a primeira execução ou duas.) Também acredito que a maior duração média da operação TRUNCATEentão DROPé atribuível às variações de carga na minha máquina local e não porque a combinação seja de alguma forma mágica. ordem de magnitude pior que as operações individuais. Afinal, eles são quase exatamente a mesma coisa.

    Se você estiver interessado em mais detalhes sobre a sobrecarga de log dessas operações, Martin tem uma explicação direta disso.

Nick Chammas
fonte
52

Testar TRUNCATEentão DROPvs apenas fazer o DROPdiretamente mostra que a primeira abordagem realmente tem um leve aumento na sobrecarga de registro, portanto pode ser levemente contraproducente.

Observar os registros de log individuais mostra que a TRUNCATE ... DROPversão é quase idêntica à DROPversão, exceto por essas entradas adicionais.

+-----------------+---------------+-------------------------+
|    Operation    |    Context    |      AllocUnitName      |
+-----------------+---------------+-------------------------+
| LOP_COUNT_DELTA | LCX_CLUSTERED | sys.sysallocunits.clust |
| LOP_COUNT_DELTA | LCX_CLUSTERED | sys.sysrowsets.clust    |
| LOP_COUNT_DELTA | LCX_CLUSTERED | sys.sysrscols.clst      |
| LOP_COUNT_DELTA | LCX_CLUSTERED | sys.sysrscols.clst      |
| LOP_HOBT_DDL    | LCX_NULL      | NULL                    |
| LOP_MODIFY_ROW  | LCX_CLUSTERED | sys.sysallocunits.clust |
| LOP_HOBT_DDL    | LCX_NULL      | NULL                    |
| LOP_MODIFY_ROW  | LCX_CLUSTERED | sys.sysrowsets.clust    |
| LOP_LOCK_XACT   | LCX_NULL      | NULL                    |
+-----------------+---------------+-------------------------+

Portanto, a TRUNCATEprimeira versão acaba desperdiçando um pouco de esforço, fazendo algumas atualizações em várias tabelas do sistema, como a seguir

  • Atualização rcmodifiedpara todas as colunas da tabela emsys.sysrscols
  • Atualização rcrowsemsysrowsets
  • Zero fora pgfirst, pgroot, pgfirstiam, pcused, pcdata, pcreservedemsys.sysallocunits

Essas linhas da tabela do sistema só acabam sendo excluídas quando a tabela é descartada na próxima instrução.

Um detalhamento completo do registro realizado por TRUNCATEvs DROPestá abaixo. Também adicionei DELETEpara fins de comparação.

+-------------------+-------------------+--------------------+------------------+-----------+---------------+-------------+------------------+-----------+---------------+-------------+
|                   |                   |                    |                            Bytes                           |                            Count                           |
+-------------------+-------------------+--------------------+------------------+-----------+---------------+-------------+------------------+-----------+---------------+-------------+
| Operation         | Context           | AllocUnitName      | Truncate / Drop  | Drop Only | Truncate Only | Delete Only | Truncate / Drop  | Drop Only | Truncate Only | Delete Only |
+-------------------+-------------------+--------------------+------------------+-----------+---------------+-------------+------------------+-----------+---------------+-------------+
| LOP_BEGIN_XACT    | LCX_NULL          |                    | 132              | 132       | 132           | 132         | 1                | 1         | 1             | 1           |
| LOP_COMMIT_XACT   | LCX_NULL          |                    | 52               | 52        | 52            | 52          | 1                | 1         | 1             | 1           |
| LOP_COUNT_DELTA   | LCX_CLUSTERED     | System Table       | 832              |           | 832           |             | 4                |           | 4             |             |
| LOP_DELETE_ROWS   | LCX_MARK_AS_GHOST | System Table       | 2864             | 2864      |               |             | 22               | 22        |               |             |
| LOP_DELETE_ROWS   | LCX_MARK_AS_GHOST | T                  |                  |           |               | 8108000     |                  |           |               | 1000        |
| LOP_HOBT_DDL      | LCX_NULL          |                    | 108              | 36        | 72            |             | 3                | 1         | 2             |             |
| LOP_LOCK_XACT     | LCX_NULL          |                    | 336              | 296       | 40            |             | 8                | 7         | 1             |             |
| LOP_MODIFY_HEADER | LCX_PFS           | Unknown Alloc Unit | 76               | 76        |               | 76          | 1                | 1         |               | 1           |
| LOP_MODIFY_ROW    | LCX_CLUSTERED     | System Table       | 644              | 348       | 296           |             | 5                | 3         | 2             |             |
| LOP_MODIFY_ROW    | LCX_IAM           | T                  | 800              | 800       | 800           |             | 8                | 8         | 8             |             |
| LOP_MODIFY_ROW    | LCX_PFS           | T                  | 11736            | 11736     | 11736         |             | 133              | 133       | 133           |             |
| LOP_MODIFY_ROW    | LCX_PFS           | Unknown Alloc Unit | 92               | 92        | 92            |             | 1                | 1         | 1             |             |
| LOP_SET_BITS      | LCX_GAM           | T                  | 9000             | 9000      | 9000          |             | 125              | 125       | 125           |             |
| LOP_SET_BITS      | LCX_IAM           | T                  | 9000             | 9000      | 9000          |             | 125              | 125       | 125           |             |
| LOP_SET_BITS      | LCX_PFS           | System Table       | 896              | 896       |               |             | 16               | 16        |               |             |
| LOP_SET_BITS      | LCX_PFS           | T                  |                  |           |               | 56000       |                  |           |               | 1000        |
| LOP_SET_BITS      | LCX_SGAM          | Unknown Alloc Unit | 168              | 224       | 168           |             | 3                | 4         | 3             |             |
+-------------------+-------------------+--------------------+------------------+-----------+---------------+-------------+------------------+-----------+---------------+-------------+
| Total             |                   |                    | 36736            | 35552     | 32220         | 8164260     | 456              | 448       | 406           | 2003        |
+-------------------+-------------------+--------------------+------------------+-----------+---------------+-------------+------------------+-----------+---------------+-------------+

O teste foi realizado em um banco de dados com modelo de recuperação completa em uma tabela de 1.000 linhas com uma linha por página. A tabela consome 1.004 páginas no total devido à página de índice raiz e 3 páginas de índice de nível intermediário.

8 dessas páginas são alocações de página única em extensões mistas, com o restante distribuído por 125 extensões uniformes. As 8 alocações de página única aparecem como as 8 LOP_MODIFY_ROW,LCX_IAMentradas de log. As 125 desalocações de extensão como LOP_SET_BITS LCX_GAM,LCX_IAM. Ambas as operações também exigem uma atualização na PFSpágina associada , portanto, as 133 LOP_MODIFY_ROW, LCX_PFSentradas combinadas . Então, quando a tabela é realmente descartada, os metadados sobre ela precisam ser removidos de várias tabelas do sistema, portanto, as 22 LOP_DELETE_ROWSentradas do log da tabela do sistema (contabilizadas como abaixo)

+----------------------+--------------+-------------------+-------------------+
|        Object        | Rows Deleted | Number of Indexes | Delete Operations |
+----------------------+--------------+-------------------+-------------------+
| sys.sysallocunits    |            1 |                 2 |                 2 |
| sys.syscolpars       |            2 |                 2 |                 4 |
| sys.sysidxstats      |            1 |                 2 |                 2 |
| sys.sysiscols        |            1 |                 2 |                 2 |
| sys.sysobjvalues     |            1 |                 1 |                 1 |
| sys.sysrowsets       |            1 |                 1 |                 1 |
| sys.sysrscols        |            2 |                 1 |                 2 |
| sys.sysschobjs       |            2 |                 4 |                 8 |
+----------------------+--------------+-------------------+-------------------+
|                      |              |                   |                22 |
+----------------------+--------------+-------------------+-------------------+

Script completo abaixo

DECLARE @Results TABLE
(
    Testing int NOT NULL,
    Operation nvarchar(31) NOT NULL,
    Context nvarchar(31)  NULL,
    AllocUnitName nvarchar(1000) NULL,
    SumLen int NULL,
    Cnt int NULL
)

DECLARE @I INT = 1

WHILE @I <= 4
BEGIN
IF OBJECT_ID('T','U') IS NULL
     CREATE TABLE T(N INT PRIMARY KEY,Filler char(8000) NULL)

INSERT INTO T(N)
SELECT DISTINCT TOP 1000 number
FROM master..spt_values


CHECKPOINT

DECLARE @allocation_unit_id BIGINT

SELECT @allocation_unit_id = allocation_unit_id
FROM   sys.partitions AS p
       INNER JOIN sys.allocation_units AS a
         ON p.hobt_id = a.container_id
WHERE  p.object_id = object_id('T')  

DECLARE @LSN NVARCHAR(25)
DECLARE @LSN_HEX NVARCHAR(25)

SELECT @LSN = MAX([Current LSN])
FROM fn_dblog(null, null)


SELECT @LSN_HEX=
        CAST(CAST(CONVERT(varbinary,SUBSTRING(@LSN, 1, 8),2) AS INT) AS VARCHAR) + ':' +
        CAST(CAST(CONVERT(varbinary,SUBSTRING(@LSN, 10, 8),2) AS INT) AS VARCHAR) + ':' +
        CAST(CAST(CONVERT(varbinary,SUBSTRING(@LSN, 19, 4),2) AS INT) AS VARCHAR)

  BEGIN TRAN
    IF @I = 1
      BEGIN
          TRUNCATE TABLE T

          DROP TABLE T
      END
    ELSE
      IF @I = 2
        BEGIN
            DROP TABLE T
        END
      ELSE
        IF @I = 3
          BEGIN
              TRUNCATE TABLE T
          END  
      ELSE
        IF @I = 4
          BEGIN
              DELETE FROM T
          END                
  COMMIT

INSERT INTO @Results
SELECT @I,
       CASE
         WHEN GROUPING(Operation) = 1 THEN 'Total'
         ELSE Operation
       END,
       Context,
       CASE
         WHEN AllocUnitId = @allocation_unit_id THEN 'T'
         WHEN AllocUnitName LIKE 'sys.%' THEN 'System Table'
         ELSE AllocUnitName
       END,
       COALESCE(SUM([Log Record Length]), 0) AS [Size in Bytes],
       COUNT(*)                              AS Cnt
FROM   fn_dblog(@LSN_HEX, null) AS D
WHERE  [Current LSN] > @LSN  
GROUP BY GROUPING SETS((Operation, Context,
       CASE
         WHEN AllocUnitId = @allocation_unit_id THEN 'T'
         WHEN AllocUnitName LIKE 'sys.%' THEN 'System Table'
         ELSE AllocUnitName
       END),())


SET @I+=1
END 

SELECT Operation,
       Context,
       AllocUnitName,
       AVG(CASE WHEN Testing = 1 THEN SumLen END) AS [Truncate / Drop Bytes],
       AVG(CASE WHEN Testing = 2 THEN SumLen END) AS [Drop Bytes],
       AVG(CASE WHEN Testing = 3 THEN SumLen END) AS [Truncate Bytes],
       AVG(CASE WHEN Testing = 4 THEN SumLen END) AS [Delete Bytes],
       AVG(CASE WHEN Testing = 1 THEN Cnt END) AS [Truncate / Drop Count],
       AVG(CASE WHEN Testing = 2 THEN Cnt END) AS [Drop Count],
       AVG(CASE WHEN Testing = 3 THEN Cnt END) AS [Truncate Count],
       AVG(CASE WHEN Testing = 4 THEN Cnt END) AS [Delete Count]              
FROM   @Results
GROUP  BY Operation,
          Context,
          AllocUnitName   
ORDER BY Operation, Context,AllocUnitName        

DROP TABLE T
Martin Smith
fonte
2

Tudo bem, pensei que eu tentaria fazer alguns benchmarks que não dependessem de "cache quente" para que, esperançosamente, eles fossem um teste mais realista (também usando o Postgres, para ver se ele corresponde às mesmas características de outras respostas postadas) :

Meus benchmarks usando o postgres 9.3.4 com um banco de dados grande, (esperançosamente grande o suficiente para não caber no cache de RAM):

Usando este script de banco de dados de teste: https://gist.github.com/rdp/8af84fbb54a430df8fc0

com 10 milhões de linhas:

truncate: 1763ms
drop: 2091ms
truncate + drop: 1763ms (truncate) + 300ms (drop) (2063ms total)
drop + recreate: 2063ms (drop) + 242ms (recreate)

com 100 milhões de linhas:

truncate: 5516ms
truncate + drop: 5592ms
drop: 5680ms (basically, the exact same ballpark)

Portanto, suponho que o seguinte: drop é "quase" tão rápido (ou mais rápido) quanto truncado + drop (pelo menos para versões modernas do Postgres); no entanto, se você planeja também virar e recriar a tabela, você pode bem, continue fazendo um truncado reto, que é mais rápido que um drop + recrie (faz sentido). FWIW.

nota 1: https://stackoverflow.com/questions/11419536/postgresql-truncation-speed/11423886#11423886 (diz que o postgres 9.2 pode ter um truncado mais rápido que as versões anteriores). Como sempre, faça benchmarks com seu próprio sistema para ver suas características.

nota 2: o truncado pode ser revertido no postgres, se estiver em uma transação: http://www.postgresql.org/docs/8.4/static/sql-truncate.html

nota 3: truncar pode, com pequenas tabelas, às vezes ser mais lento que uma exclusão: https://stackoverflow.com/questions/11419536/postgresql-truncation-speed/11423886#11423886

rogerdpack
fonte
1

Adicionando alguma perspectiva histórica ...

A eliminação de uma tabela requer a atualização de várias tabelas do sistema, o que normalmente exige que essas alterações sejam alteradas em uma única transação (pense em "começar tran, excluir syscolumns, excluir sysobjects, confirmar").

Também incluída na 'tabela suspensa' está a necessidade de desalocar todas as páginas de dados / índice associadas à tabela.

Muitos, muitos, muitos anos atrás ... o processo de desalocação de espaço foi incluído na transação que também atualizou as tabelas do sistema; o resultado líquido foi que quanto maior o número de páginas alocadas, mais tempo demorava para desalocar as páginas, mais tempo A transação (nas tabelas do sistema) foi deixada em aberto e, portanto, uma maior chance de bloquear (nas tabelas do sistema) outros processos que tentam criar / soltar tabelas no tempdb (especialmente desagradável com as páginas mais antigas == bloqueio no nível da página e potencial para a tabela escalonamento de nível de bloqueio).

Um método antigo usado (na época) para reduzir a contenção nas tabelas do sistema era reduzir o tempo em que os bloqueios eram mantidos nas tabelas do sistema e uma maneira (relativamente) fácil de fazer isso era desalocar as páginas de dados / índice antes de soltar a mesa.

Embora truncate tablenão desaloque todas as páginas de dados / índice, desaloca apenas uma extensão de 8 páginas (dados); outro 'hack' foi então eliminar todos os índices antes de soltar a tabela (sim, separar o txn nos sysindexes, mas um txn menor para o drop table).

Quando você considera que (novamente, muitos, muitos anos atrás), havia apenas o único banco de dados 'tempdb', e alguns aplicativos fizeram HEAVY usar esse único banco de dados 'tempdb', quaisquer 'hacks' que pudessem reduzir a contenção nas tabelas do sistema em 'tempdb' foram benéficos; com o tempo, as coisas melhoraram ... vários bancos de dados temporários, bloqueio em nível de linha nas tabelas do sistema, melhores métodos de desalocação, etc.

Enquanto isso, o uso do truncate tablenão prejudica nada se deixado no código.

markp
fonte
-2

Faz sentido executar TRUNCATE para tabelas que possuem chaves estrangeiras. No entanto, para tabelas temporárias, apenas DROP é suficiente

Evgeniy Gribkov
fonte
TRUNCATE evitaria de alguma forma um conflito de chave estrangeira? Quão?
user259412 16/08
1
Irá escrever um erro que há chave estrangeira
Evgeniy Gribkov
-8

O objetivo truncateé remover tudo de forma simples e irrevogável da tabela (algumas especificações técnicas baseadas nos mecanismos de armazenamento de dados podem diferir ligeiramente) - ignorando o registro pesado, etc.

drop tableregistra todas as alterações conforme as alterações estão sendo feitas. Portanto, para ter um registro mínimo e reduzir a rotatividade inútil do sistema, eu suspeitaria que uma tabela muito grande pudesse ser truncada primeiro e depois descartada.

truncate pode ser agrupado em uma transação (que seria a opção mais segura) que, é claro, permitirá reverter a operação.

IAbstract
fonte