Por que os TVPs devem ser READONLY e por que parâmetros de outros tipos não podem ser READONLY

19

De acordo com este blog, os parâmetros de uma função ou procedimento armazenado são essencialmente passados ​​por valor, se não forem OUTPUTparâmetros, e essencialmente tratados como uma versão mais segura do passe por referência, se forem OUTPUTparâmetros.

No começo, pensei que o objetivo de forçar a TVP a ser declarada READONLYera sinalizar claramente aos desenvolvedores que a TVP não pode ser usada como OUTPUTparâmetro, mas deve haver mais coisas acontecendo porque não podemos declarar que não é TVP READONLY. Por exemplo, o seguinte falha:

create procedure [dbo].[test]
@a int readonly
as
    select @a

Msg 346, Nível 15, Estado 1, Teste de procedimento
O parâmetro "@a" não pode ser declarado READONLY, pois não é um parâmetro com valor de tabela.

  1. Como as estatísticas não são armazenadas no TVP, qual é a lógica por trás da prevenção de operações DML?
  2. Está relacionado a não querer que o TVP seja OUTPUTparâmetros por algum motivo?
Erik
fonte

Respostas:

19

A explicação parece estar ligada a uma combinação de: a) um detalhe do blog vinculado que não foi mencionado nesta questão, b) a pragmática dos TVPs que se encaixam em como os parâmetros sempre são passados ​​para dentro e para fora, c) e a natureza das variáveis ​​da tabela.

  1. Os detalhes ausentes contidos na postagem do blog vinculado são exatamente como as variáveis ​​são passadas dentro e fora dos Procedimentos e Funções Armazenadas (que se relacionam ao fraseado na Questão de "uma versão mais segura da passagem por referência se forem parâmetros de SAÍDA") :

    O TSQL usa uma semântica de cópia / entrada / saída para passar parâmetros para procedimentos e funções armazenados.

    ... quando o processo armazenado termina a execução (sem pressionar um erro), é feita uma cópia que atualiza o parâmetro transmitido com todas as alterações feitas no processo armazenado.

    O benefício real dessa abordagem está no caso de erro. Se ocorrer um erro no meio da execução de um procedimento armazenado, quaisquer alterações feitas nos parâmetros não serão propagadas de volta para o chamador.

    Se a palavra-chave OUTPUT não estiver presente, nenhuma cópia será feita.

    A linha inferior: os
    parâmetros para procs armazenados nunca refletem a execução parcial do proc armazenado se ele encontrar um erro.

    A parte 1 deste quebra-cabeça é que os parâmetros sempre são passados ​​"por valor". E somente quando o parâmetro é marcado como OUTPUT e o Procedimento Armazenado é concluído com êxito é que o valor atual é realmente enviado de volta. Se os OUTPUTvalores fossem realmente passados ​​"por referência", o ponteiro para o local na memória dessa variável seria o que foi passado, não o valor em si. E se você passar o ponteiro (ou seja, endereço de memória), todas as alterações feitas serão imediatamente refletidas, mesmo que a próxima linha do Procedimento Armazenado cause um erro e interrompa a execução.

    Para resumir a Parte 1: os valores das variáveis ​​são sempre copiados; eles não são referenciados pelo endereço de memória.

  2. Com a Parte 1 em mente, uma política de sempre copiar valores de variáveis ​​pode levar a problemas de recursos quando a variável que está sendo transmitida é bastante grande. Eu não testei para ver como tipos blob são manipulados ( VARCHAR(MAX), NVARCHAR(MAX), VARBINARY(MAX), XML, e aqueles que não deve ser mais utilizado: TEXT, NTEXT, e IMAGE), mas é seguro dizer que qualquer tabela de dados sendo passados em poderia ser muito grande. Faz sentido que aqueles que desenvolvem o recurso TVP desejem uma verdadeira capacidade de "passar por referência" para impedir que seu novo recurso destrua um número saudável de sistemas (ou seja, deseje uma abordagem mais escalável). Como você pode ver na documentação, foi o que eles fizeram:

    O Transact-SQL passa parâmetros com valor de tabela para rotinas por referência para evitar fazer uma cópia dos dados de entrada.

    Além disso, esse problema de gerenciamento de memória não era um conceito novo, pois pode ser encontrado na API SQLCLR que foi introduzida no SQL Server 2005 (os TVPs foram introduzidos no SQL Server 2008). Ao transmitir NVARCHARe VARBINARYdados para o código SQLCLR (ou seja, parâmetros de entrada nos métodos .NET dentro de um assembly SQLCLR), você tem a opção de seguir a abordagem "por valor" usando um SqlStringou outro SqlBinary, respectivamente, ou pode usar o "por referência "usando um SqlCharsou SqlBytesrespectivamente. Os tipos SqlCharse SqlBytespermitem o fluxo completo de dados no .NET CLR, de modo que você pode extrair pequenos blocos de valores grandes em vez de copiar um valor inteiro de 200 MB (até 2 GB, à direita).

    Para resumir a Parte 2: os TVPs, por sua própria natureza, teriam propensão a consumir muita memória (e, portanto, deteriorar o desempenho) se permanecer no modelo "sempre copie o valor". Portanto, os TVPs fazem um verdadeiro "passe por referência".

  3. A parte final é por que a Parte 2 importa: por que a transmissão de um TVP realmente "por referência" em vez de fazer uma cópia dele mudaria alguma coisa. E isso é respondido pelo objetivo do projeto que é a base da Parte 1: Procedimentos armazenados que não são concluídos com êxito, não deve alterar de forma alguma nenhum dos parâmetros de entrada, estejam eles marcados OUTPUTou não. Permitir operações DML afetaria imediatamente o valor do TVP, pois ele existe no contexto de chamada (uma vez que passar por referência significa que você está alterando o que foi passado, não uma cópia do que foi passado).

    Agora, alguém, em algum lugar, provavelmente está conversando com o monitor dizendo: "Bem, basta criar um mecanismo de automagia para reverter as alterações feitas nos parâmetros da TVP, se alguma foi passada para o Stored Procedure. Duh. Problema resolvido." Não tão rápido. É aqui que entra a natureza das variáveis ​​de tabela: as alterações feitas nas variáveis ​​de tabela não são vinculadas por transações! Portanto, não há como reverter as alterações. E, de fato, esse é um truque usado para salvar as informações geradas em uma transação, caso seja necessário reverter :-).

    Para resumir a Parte 3: Variáveis ​​de Tabela não permitem "desfazer" alterações feitas a elas no caso de um erro que faz com que o Procedimento Armazenado seja interrompido. E isso viola o objetivo do projeto de ter parâmetros que nunca refletem a execução parcial (Parte 1).

Ergo: a READONLYpalavra-chave é necessária para impedir operações DML nos TVPs, pois são variáveis ​​de tabela que são realmente passadas "por referência" e, portanto, quaisquer modificações nelas seriam imediatamente refletidas, mesmo se o procedimento armazenado encontrar um erro e não houver outra maneira de evitar isso.

Além disso, os parâmetros de outros tipos de dados não podem ser usados READONLYporque já são cópias do que foi passado e, portanto, não protegem nada que ainda não esteja protegido. Isso e a maneira como os parâmetros dos outros tipos de dados funcionam deveriam ser de leitura e gravação, portanto, provavelmente seria ainda mais trabalhoso alterar essa API para incluir agora um conceito somente leitura.

Solomon Rutzky
fonte
Explicação muito detalhada. Obrigado. Portanto, não há como modificar uma variável de tabela passada (uma TYPEvariável TVP do usuário ou a DECLARE x as TABLE (...)) com um procedimento armazenado? Posso fazê-lo, embora com maior espaço de memória, com uma função em vez de set @tvp = myfunction(@tvp)se o RETURNSvalor da minha função for uma tabela com o mesmo DDL do tipo TVP?
MPAG
@mpag Obrigado. Um TVP é uma variável de tabela, não há diferença. Você não passa o tipo, passa uma variável de tabela criada a partir de um tipo ou de uma declaração de esquema explícita. Além disso, você não pode SETuma variável de tabela, pelo menos não que eu saiba. E mesmo se você pudesse: a) você não pode acessar um conjunto de resultados através do =operador eb) o TVP ainda está marcado como READONLY, portanto, configurá-lo violaria isso. Apenas despeje o conteúdo em uma tabela temporária ou outra variável de tabela que você criar dentro do proc.
Solomon Rutzky
Obrigado novamente. Decidi usar essencialmente uma abordagem de tabela temporária.
MPAG
5

Resposta do Community Wiki gerada a partir de um comentário sobre a pergunta de Martin Smith

Há um item ativo do Connect (enviado por Erland Sommarskog) para isso:

Relaxe a restrição de que os parâmetros da tabela devem ser somente leitura quando os SPs se chamam

A única resposta da Microsoft até agora diz (grifo nosso):

Obrigado pelo feedback sobre isso. Recebemos feedback semelhante de um grande número de clientes. Permitir que os parâmetros com valor de tabela sejam lidos / gravados envolve bastante trabalho do lado do Mecanismo SQL, bem como protocolos de clientes. Devido a restrições de tempo / recursos, além de outras prioridades, não poderemos retomar esse trabalho como parte da versão do SQL Server 2008. No entanto, investigamos esse problema e temos isso firmemente em nosso radar para resolver como parte da próxima versão do SQL Server. Agradecemos e agradecemos o feedback aqui.

Srini Acharya
Gerente sênior de programas
SQL Server Relational Engine

Paul White
fonte