SQL Server中的临时表和表变量有什么区别?


Answers:


668

内容

内容

警告

此答案讨论了SQL Server 2000中引入的“经典”表变量。内存OLTP中的SQL Server 2014引入了内存优化表类型。这些变量的表变量实例在许多方面与下面讨论的变量实例不同!(更多详细信息)。

存储位置

没有不同。两者都存储在中tempdb

我已经看到它建议,对于表变量,情况并非总是如此,但这可以从下面进行验证

DECLARE @T TABLE(X INT)

INSERT INTO @T VALUES(1),(2)

SELECT sys.fn_PhysLocFormatter(%%physloc%%) AS [File:Page:Slot]
FROM @T

结果示例(tempdb存储两行中的显示位置)

File:Page:Slot
----------------
(1:148:0)
(1:148:1)

逻辑位置

@table_variables行为比#temp表更像它们是当前数据库的一部分。对于表变量(自2005年起),如果未明确指定,则列排序规则将是当前数据库的列排序规则,而对于#temp表,它将使用默认的排序规则tempdb更多详细信息)。此外,用户定义的数据类型和XML集合必须在tempdb中才能用于#temp表,但是表变量可以从当前数据库(Source)中使用它们。

SQL Server 2012引入了包含的数据库。这些临时表的行为有所不同(h / t Aaron)

在包含的数据库中,临时表数据以包含的数据库的排序规则进行排序。

  • 与临时表关联的所有元数据(例如,表和列名,索引等)将在目录整理中。
  • 命名约束不能在临时表中使用。
  • 临时表可能不引用用户定义的类型,XML模式集合或用户定义的函数。

对不同范围的可见性

@table_variables只能在声明它们的批次和范围内访问。#temp_tables可在子批处理中访问(嵌套触发器,过程,exec调用)。#temp_tables在外部作用域(@@NESTLEVEL=0)中创建的批处理也可以跨越批次,因为它们将持续到会话结束。不能在子批处理中创建任何类型的对象,也不能在调用范围内访问这两种对象,但是如下所述(可以是全局##temp表)。

一生

@table_variablesDECLARE @.. TABLE执行包含语句的批处理时(在该批处理中的任何用户代码运行之前)隐式创建,并在末尾隐式删除。

尽管解析器不允许您在DECLARE语句之前尝试使用表变量,但可以在下面看到隐式创建。

IF (1 = 0)
BEGIN
DECLARE @T TABLE(X INT)
END

--Works fine
SELECT *
FROM @T

#temp_tables是在CREATE TABLE遇到TSQL 语句时显式创建的,可以用显式删除,DROP TABLE或在批处理结束时(如果使用,在子批处理中创建@@NESTLEVEL > 0)或会话以其他方式结束时隐式删除。

注意:在存储的例程中,两种类型的对象都可以缓存,而不是重复创建和删除新表。关于何时可以进行此缓存存在一些限制,但是可能会违反#temp_tables这些限制,但是@table_variables无论如何都将阻止这些限制。缓存#temp表的维护开销略大于表变量的维护开销,如此处所示

对象元数据

对于两种类型的对象,这基本上是相同的。它存储在中的系统基本表中tempdb。可以很容易地看到一个#temp表,但是 OBJECT_ID('tempdb..#T')可以用来键入系统表,并且内部生成的名称与该CREATE TABLE语句中定义的名称更紧密相关。对于表变量,该object_id功能不起作用,内部名称完全由系统生成,与变量名称无关。下面通过键入(希望唯一)列名来演示元数据仍然存在。对于没有唯一列名的表DBCC PAGE,只要它们不为空,就可以使用object_id来确定。

/*Declare a table variable with some unusual options.*/
DECLARE @T TABLE
(
[dba.se] INT IDENTITY PRIMARY KEY NONCLUSTERED,
A INT CHECK (A > 0),
B INT DEFAULT 1,
InRowFiller char(1000) DEFAULT REPLICATE('A',1000),
OffRowFiller varchar(8000) DEFAULT REPLICATE('B',8000),
LOBFiller varchar(max) DEFAULT REPLICATE(cast('C' as varchar(max)),10000),
UNIQUE CLUSTERED (A,B) 
    WITH (FILLFACTOR = 80, 
         IGNORE_DUP_KEY = ON, 
         DATA_COMPRESSION = PAGE, 
         ALLOW_ROW_LOCKS=ON, 
         ALLOW_PAGE_LOCKS=ON)
)

INSERT INTO @T (A)
VALUES (1),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13)

SELECT t.object_id,
       t.name,
       p.rows,
       a.type_desc,
       a.total_pages,
       a.used_pages,
       a.data_pages,
       p.data_compression_desc
FROM   tempdb.sys.partitions AS p
       INNER JOIN tempdb.sys.system_internals_allocation_units AS a
         ON p.hobt_id = a.container_id
       INNER JOIN tempdb.sys.tables AS t
         ON t.object_id = p.object_id
       INNER JOIN tempdb.sys.columns AS c
         ON c.object_id = p.object_id
WHERE  c.name = 'dba.se'

输出量

Duplicate key was ignored.

 +-----------+-----------+------+-------------------+-------------+------------+------------+-----------------------+
| object_id |   name    | rows |     type_desc     | total_pages | used_pages | data_pages | data_compression_desc |
+-----------+-----------+------+-------------------+-------------+------------+------------+-----------------------+
| 574625090 | #22401542 |   13 | IN_ROW_DATA       |           2 |          2 |          1 | PAGE                  |
| 574625090 | #22401542 |   13 | LOB_DATA          |          24 |         19 |          0 | PAGE                  |
| 574625090 | #22401542 |   13 | ROW_OVERFLOW_DATA |          16 |         14 |          0 | PAGE                  |
| 574625090 | #22401542 |   13 | IN_ROW_DATA       |           2 |          2 |          1 | NONE                  |
+-----------+-----------+------+-------------------+-------------+------------+------------+-----------------------+

交易次数

操作@table_variables作为系统事务执行,独立于任何外部用户事务,而等效#temp表操作将作为用户事务本身的一部分执行。因此,ROLLBACK命令将影响#temp表,但@table_variable保持不变。

DECLARE @T TABLE(X INT)
CREATE TABLE #T(X INT)

BEGIN TRAN

INSERT #T
OUTPUT INSERTED.X INTO @T
VALUES(1),(2),(3)

/*Both have 3 rows*/
SELECT * FROM #T
SELECT * FROM @T

ROLLBACK

/*Only table variable now has rows*/
SELECT * FROM #T
SELECT * FROM @T
DROP TABLE #T

记录中

两者都生成到tempdb事务日志的日志记录。一个常见的误解是表变量不是这种情况,因此下面的脚本演示了这一点,它声明了一个表变量,添加了几行,然后对其进行了更新并删除了它们。

由于表变量是在批处理的开始和结束时隐式创建和删除的,因此有必要使用多个批处理以查看完整的日志记录。

USE tempdb;

/*
Don't run this on a busy server.
Ideally should be no concurrent activity at all
*/
CHECKPOINT;

GO

/*
The 2nd column is binary to allow easier correlation with log output shown later*/
DECLARE @T TABLE ([C71ACF0B-47E9-4CAD-9A1E-0C687A8F9CF3] INT, B BINARY(10))

INSERT INTO @T
VALUES (1, 0x41414141414141414141), 
       (2, 0x41414141414141414141)

UPDATE @T
SET    B = 0x42424242424242424242

DELETE FROM @T

/*Put allocation_unit_id into CONTEXT_INFO to access in next batch*/
DECLARE @allocId BIGINT, @Context_Info VARBINARY(128)

SELECT @Context_Info = allocation_unit_id,
       @allocId = a.allocation_unit_id 
FROM   sys.system_internals_allocation_units a
       INNER JOIN sys.partitions p
         ON p.hobt_id = a.container_id
       INNER JOIN sys.columns c
         ON c.object_id = p.object_id
WHERE  ( c.name = 'C71ACF0B-47E9-4CAD-9A1E-0C687A8F9CF3' )

SET CONTEXT_INFO @Context_Info

/*Check log for records related to modifications of table variable itself*/
SELECT Operation,
       Context,
       AllocUnitName,
       [RowLog Contents 0],
       [Log Record Length]
FROM   fn_dblog(NULL, NULL)
WHERE  AllocUnitId = @allocId

GO

/*Check total log usage including updates against system tables*/
DECLARE @allocId BIGINT = CAST(CONTEXT_INFO() AS BINARY(8));

WITH T
     AS (SELECT Operation,
                Context,
                CASE
                  WHEN AllocUnitId = @allocId THEN 'Table Variable'
                  WHEN AllocUnitName LIKE 'sys.%' THEN 'System Base Table'
                  ELSE AllocUnitName
                END AS AllocUnitName,
                [Log Record Length]
         FROM   fn_dblog(NULL, NULL) AS D)
SELECT Operation = CASE
                     WHEN GROUPING(Operation) = 1 THEN 'Total'
                     ELSE Operation
                   END,
       Context,
       AllocUnitName,
       [Size in Bytes] = COALESCE(SUM([Log Record Length]), 0),
       Cnt = COUNT(*)
FROM   T
GROUP  BY GROUPING SETS( ( Operation, Context, AllocUnitName ), ( ) )
ORDER  BY GROUPING(Operation),
          AllocUnitName 

退货

详细视图

结果截图

摘要视图(包括隐式删除表和系统基表的日志记录)

结果截图

据我所知,对两者的操作都能生成大致相等的日志记录。

尽管日志记录的数量非常相似,但一个重要的区别是,与#temp表相关的日志记录要等到任何包含用户事务的事务完成后才能清除,因此长时间运行的事务(有时#temp会写入表)将防止日志被截断,tempdb而自主事务生成的表变量没有。

表变量不支持,TRUNCATE因此在要求从表中删除所有行时可能会不利于日志记录(尽管对于很小的表DELETE ,反而效果更好

基数

许多涉及表变量的执行计划将显示一行估计值,作为它们的输出。检查表变量属性显示的SQL Server认为该表变量具有行(为什么它估计1行将从零行的表发出由@保罗白解释这里)。

但是,上一节中显示的结果确实显示了的准确rows计数sys.partitions。问题在于,在大多数情况下,引用表变量的语句在表为空时进行编译。如果该语句在@table_variable填充后被(重新)编译,则将其用于表基数(这可能是由于显式的,recompile或者可能是因为该语句还引用了另一个导致延迟编译或重新编译的对象)。

DECLARE @T TABLE(I INT);

INSERT INTO @T VALUES(1),(2),(3),(4),(5)

CREATE TABLE #T(I INT)

/*Reference to #T means this statement is subject to deferred compile*/
SELECT * FROM @T WHERE NOT EXISTS(SELECT * FROM #T)

DROP TABLE #T

计划显示延迟的编译后准确的估计行数。

显示准确的行数

在SQL Server 2012 SP2中,引入了跟踪标志2453。更多详细信息在此处的 “关系引擎”下。

启用此跟踪标志后,它可能导致自动重新编译考虑更改的基数,这将在不久之后进一步讨论。

注意:在Azure的兼容级别150中,该语句的编译现在推迟到第一次执行。这意味着它将不再受到零行估计问题的困扰。

无栏统计

具有更准确的表基数并不意味着估计的行数会更准确(除非对表中的所有行都执行操作)。SQL Server根本不维护表变量的列统计信息,因此将基于比较谓词(例如,=针对非唯一列返回表的10%或进行>比较时返回表的10%)。相反维护#temp表的列统计信息。

SQL Server维护对每一列所做的修改数量的计数。如果自计划编译以来的修改次数超过了重新编译阈值(RT),则将重新编译计划并更新统计信息。RT取决于表的类型和大小。

SQL Server 2008中的计划缓存

RT计算如下。(n表示编译查询计划时表的基数。)

永久表
-如果n <= 500,RT =500。-
如果n> 500,RT = 500 + 0.20 * n。

临时表
-如果n <6,RT =6。-
如果6 <= n <= 500,RT =500。-
如果n> 500,RT = 500 + 0.20 * n。
表变量
-RT不存在。因此,由于表变量基数的更改,不会重新编译。 (但请参阅下面有关TF 2453的注释)

KEEP PLAN提示可用于设定室温#temp的表一样的永久表。

所有这些的最终结果是,为#temp表生成的执行计划通常比@table_variables涉及许多行时要好几个数量级,因为SQL Server具有更好的信息来处理。

NB1:表变量没有统计信息,但仍会在跟踪标志2453下引发“ Statistics Changed”重新编译事件(不适用于“琐碎”计划),这似乎是在与上述临时表相同的重新编译阈值下发生的,并带有一个如果是另外一个N=0 -> RT = 1。即,当表变量为空时,所有已编译的语句最终将获得重新编译并TableCardinality在非空时首次执行时更正。编译时间表的基数存储在计划中,并且如果再次以相同的基数执行该语句(由于控制语句流或缓存的计划的重用),则不会重新编译。

NB2:对于存储过程中缓存的临时表,重新编译的过程比上面描述的要复杂得多。有关所有详细信息,请参见存储过程中的临时表

重新编译

以及上述基于修改重新编译#temp表也可以与相关联的额外的编译仅仅是因为它们允许被禁止用于触发一个编译表变量的操作(例如DDL的变化CREATE INDEXALTER TABLE

锁定

已经指出该表变量不参与锁定。不是这种情况。将以下输出运行到SSMS消息选项卡,以获取针对插入语句获取和释放的锁的详细信息。

DECLARE @tv_target TABLE (c11 int, c22 char(100))

DBCC TRACEON(1200,-1,3604)

INSERT INTO @tv_target (c11, c22)

VALUES (1, REPLICATE('A',100)), (2, REPLICATE('A',100))

DBCC TRACEOFF(1200,-1,3604)

对于SELECT来自表变量的查询,Paul White在注释中指出,这些查询自动带有隐式NOLOCK提示。如下所示

DECLARE @T TABLE(X INT); 

SELECT X
FROM @T 
OPTION (RECOMPILE, QUERYTRACEON 3604, QUERYTRACEON 8607)

输出量

*** Output Tree: (trivial plan) ***

        PhyOp_TableScan TBL: @T Bmk ( Bmk1000) IsRow: COL: IsBaseRow1002  Hints( NOLOCK )

但是,这对锁定的影响可能很小。

SET NOCOUNT ON;

CREATE TABLE #T( [ID] [int] IDENTITY NOT NULL,
                 [Filler] [char](8000) NULL,
                 PRIMARY KEY CLUSTERED ([ID] DESC))


DECLARE @T TABLE ( [ID] [int] IDENTITY NOT NULL,
                 [Filler] [char](8000) NULL,
                 PRIMARY KEY CLUSTERED ([ID] DESC))

DECLARE @I INT = 0

WHILE (@I < 10000)
BEGIN
INSERT INTO #T DEFAULT VALUES
INSERT INTO @T DEFAULT VALUES
SET @I += 1
END

/*Run once so compilation output doesn't appear in lock output*/
EXEC('SELECT *, sys.fn_PhysLocFormatter(%%physloc%%) FROM #T')

DBCC TRACEON(1200,3604,-1)
SELECT *, sys.fn_PhysLocFormatter(%%physloc%%)
FROM @T

PRINT '--*--'

EXEC('SELECT *, sys.fn_PhysLocFormatter(%%physloc%%) FROM #T')

DBCC TRACEOFF(1200,3604,-1)

DROP TABLE #T

这些返回结果均未按索引键顺序指示SQL Server 对两者都使用了分配顺序扫描

我两次运行以上脚本,第二次运行的结果如下

Process 58 acquiring Sch-S lock on OBJECT: 2:-1325894110:0  (class bit0 ref1) result: OK

--*--
Process 58 acquiring IS lock on OBJECT: 2:-1293893996:0  (class bit0 ref1) result: OK

Process 58 acquiring S lock on OBJECT: 2:-1293893996:0  (class bit0 ref1) result: OK

Process 58 releasing lock on OBJECT: 2:-1293893996:0 

由于SQL Server只是在对象上获得了架构稳定性锁,因此表变量的锁输出确实极少。但是对于一张#temp桌子来说,它几乎是轻巧的,因为它取出了一个对象级S锁。甲NOLOCK提示或READ UNCOMMITTED隔离级别当然可与工作时明确指定#temp的表,以及。

与记录周围的用户事务的问题类似,这意味着表的锁定时间更长#temp。使用以下脚本

    --BEGIN TRAN;   

    CREATE TABLE #T (X INT,Y CHAR(4000) NULL);

    INSERT INTO #T (X) VALUES(1) 

    SELECT CASE resource_type
             WHEN  'OBJECT' THEN OBJECT_NAME(resource_associated_entity_id, 2)
             WHEN  'ALLOCATION_UNIT' THEN (SELECT OBJECT_NAME(object_id, 2)
                                           FROM  tempdb.sys.allocation_units a 
                                           JOIN tempdb.sys.partitions p ON a.container_id = p.hobt_id
                                           WHERE  a.allocation_unit_id = resource_associated_entity_id)
             WHEN 'DATABASE' THEN DB_NAME(resource_database_id)                                      
             ELSE (SELECT OBJECT_NAME(object_id, 2)
                   FROM   tempdb.sys.partitions
                   WHERE  partition_id = resource_associated_entity_id)
           END AS object_name,
           *
    FROM   sys.dm_tran_locks
    WHERE  request_session_id = @@SPID

    DROP TABLE #T

   -- ROLLBACK  

在这两种情况下都在显式用户事务之外运行时,检查时返回的唯一锁sys.dm_tran_locks是上的共享锁DATABASE

取消注释后,BEGIN TRAN ... ROLLBACK将返回26行,这表明在对象本身和系统表行上均保留了锁,以允许回滚并防止其他事务读取未提交的数据。等效的表变量操作不随用户事务而回滚,并且不需要持有这些锁以供我们检入下一条语句,但是跟踪在Profiler中获取和释放的锁或使用跟踪标志1200仍显示出很多锁事件发生。

指标

对于SQL Server 2014之前的版本,只能在表变量上隐式创建索引,这是添加唯一约束或主键的副作用。当然,这确实意味着仅支持唯一索引。可以模拟表上具有唯一聚集索引的非唯一非聚集索引,方法是简单地声明它UNIQUE NONCLUSTERED,并将CI键添加到所需NCI键的末尾(即使非唯一,SQL Server也会在后台执行此操作可以指定NCI)

如前所述,index_option可以在约束声明中指定各种s,包括DATA_COMPRESSIONIGNORE_DUP_KEYFILLFACTOR(尽管设置一个s 没有意义,因为它只会对索引重建产生任何影响,而您不能重建表变量的索引!)

此外,表变量不支持INCLUDEd列,过滤索引(到2016年)或分区,#temp表不支持(必须在中创建分区方案tempdb)。

SQL Server 2014中的索引

可以在SQL Server 2014的表变量定义中内联声明非唯一索引。下面是此示例的语法。

DECLARE @T TABLE (
C1 INT INDEX IX1 CLUSTERED, /*Single column indexes can be declared next to the column*/
C2 INT INDEX IX2 NONCLUSTERED,
       INDEX IX3 NONCLUSTERED(C1,C2) /*Example composite index*/
);

SQL Server 2016中的索引

从CTP 3.1开始,现在可以声明表变量的过滤索引。通过RTM,可能会允许包含的列,尽管由于资源限制它们很可能不会将其包含在SQL16中

DECLARE @T TABLE
(
c1 INT NULL INDEX ix UNIQUE WHERE c1 IS NOT NULL /*Unique ignoring nulls*/
)

平行性

插入(或以其他方式修改)的查询@table_variables不能具有并行计划,#temp_tables并且不以此方式进行限制。

有一个明显的解决方法,即如下重写确实允许SELECT零件并行发生,但最终使用了隐藏的临时表(在幕后)。

INSERT INTO @DATA ( ... ) 
EXEC('SELECT .. FROM ...')

从表变量中选择的查询没有这种限制,如我的答案所示

其他功能差异

  • #temp_tables不能在函数内部使用。@table_variables可以在标量或多语句表UDF中使用。
  • @table_variables 不能有命名约束。
  • @table_variables不能为SELECT-ed INTOALTER-ed,TRUNCATEd或为DBCC命令的目标,例如DBCC CHECKIDENT或,SET IDENTITY INSERT并且不支持表提示例如WITH (FORCESCAN)
  • CHECK 为了简化,隐式谓词或矛盾检测,优化器未考虑对表变量的约束。
  • 表变量似乎不符合行集共享优化的条件,这意味着针对这些变量进行删除和更新计划会遇到更多的开销和PAGELATCH_EX等待。(示例

仅记忆?

如开头所述,两者都存储在中的页面上tempdb。但是,我没有解决在将这些页面写入光盘时在行为上是否有任何区别。

我现在对此进行了少量测试,到目前为止,还没有发现任何区别。在我对SQL Server 250页面实例进行的特定测试中,似乎是写入数据文件之前的切入点。

注意:以下行为在SQL Server 2014或SQL Server 2012 SP1 / CU10或SP2 / CU1中不再发生,因此,急切的编写器不再像将页写到光盘那样急切。有关该更改的更多详细信息,请参见SQL Server 2014:tempdb隐藏性能宝石

运行以下脚本

CREATE TABLE #T(X INT, Filler char(8000) NULL)
INSERT INTO #T(X)
SELECT TOP 250 ROW_NUMBER() OVER (ORDER BY @@SPID)
FROM master..spt_values
DROP TABLE #T

tempdb使用Process Monitor 监视监视写入数据文件时,我看不到(偶尔在偏移量73,728处的数据库引导页中除外)。更改250为以后,251我开始看到如下内容。

ProcMon

上面的屏幕截图显示了5 * 32页写操作和一个单页写操作,表明有161页写到了磁盘。使用表变量进行测试时,我得到了250页的截止点。下面的脚本通过查看以下内容以不同的方式显示它sys.dm_os_buffer_descriptors

DECLARE @T TABLE (
  X        INT,
  [dba.se] CHAR(8000) NULL)

INSERT INTO @T
            (X)
SELECT TOP 251 Row_number() OVER (ORDER BY (SELECT 0))
FROM   master..spt_values

SELECT is_modified,
       Count(*) AS page_count
FROM   sys.dm_os_buffer_descriptors
WHERE  database_id = 2
       AND allocation_unit_id = (SELECT a.allocation_unit_id
                                 FROM   tempdb.sys.partitions AS p
                               INNER JOIN tempdb.sys.system_internals_allocation_units AS a
                                          ON p.hobt_id = a.container_id
                                        INNER JOIN tempdb.sys.columns AS c
                                          ON c.object_id = p.object_id
                                 WHERE  c.name = 'dba.se')
GROUP  BY is_modified 

结果

is_modified page_count
----------- -----------
0           192
1           61

显示192页已写入光盘,脏标记被清除。它还表明,写入磁盘并不意味着页面将立即从缓冲池中退出。仍然可以完全从内存中满足对此表变量的查询。

max server memory设置为2000 MBDBCC MEMORYSTATUS报告缓冲池页面的空闲服务器上,分配的缓冲池页面约为1,843,000 KB(约23,000页),我以1,000行/页的数量批量插入了上述表格,并记录了每次迭代。

SELECT Count(*)
FROM   sys.dm_os_buffer_descriptors
WHERE  database_id = 2
       AND allocation_unit_id = @allocId
       AND page_type = 'DATA_PAGE' 

table变量和#temptable都给出了几乎相同的图形,并设法使缓冲池最大程度地溢出,直到它们没有完全保留在内存中为止,因此似乎对多少内存没有特别的限制。两者都可以消耗。

缓冲池中的页面


我发现与临时表变量相比,SQL Server在创建临时表(甚至具有缓存)时获得的闩锁数量要多得多。您可以通过使用闩锁获取的调试XE并创建一个包含约35列的表来进行测试。我发现表变量占用4个锁存器,而临时表占用70个左右锁存器。
Joe Obbish

40

我想指出的几件事更多是基于特定的经验而不是学习。作为一名DBA,我是新手,因此请在需要时进行纠正。

  1. 默认情况下,#temp表使用SQL Server实例的默认排序规则。因此,除非另外指定,否则如果masterdb与数据库的排序规则不同,则可能会在比较或更新#temp表和数据库表之间的值时遇到问题。请参阅:http : //www.mssqltips.com/sqlservertip/2440/create-sql-server-temporary-tables-with-the-correct-collat​​ion/
  2. 完全基于个人经验,可用内存似乎会影响其性能。MSDN建议使用表变量来存储较小的结果集,但是在大多数情况下,差异甚至不明显。但是,在较大的集合中,在某些情况下,表变量的内存消耗明显增加,并且可能使查询变慢以进行爬网,这一点已变得显而易见。

6
还要注意,如果使用SQL Server 2012并且包含数据库,则#temp表的排序规则可以继承调用数据库的排序规则。
亚伦·伯特兰

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.