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.
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 OUTPUT
valores 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.
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 NVARCHAR
e VARBINARY
dados 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 SqlString
ou outro SqlBinary
, respectivamente, ou pode usar o "por referência "usando um SqlChars
ou SqlBytes
respectivamente. Os tipos SqlChars
e SqlBytes
permitem 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".
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 OUTPUT
ou 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 READONLY
palavra-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 READONLY
porque 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.
TYPE
variável TVP do usuário ou aDECLARE x as TABLE (...)
) com um procedimento armazenado? Posso fazê-lo, embora com maior espaço de memória, com uma função em vez deset @tvp = myfunction(@tvp)
se oRETURNS
valor da minha função for uma tabela com o mesmo DDL do tipo TVP?SET
uma 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 comoREADONLY
, 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.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):
fonte