无法在分区函数中使用 datetime2(0) 恢复数据库

Ole*_*leg 4 sql-server partitioning restore datetime2 sql-server-2017

我有一个 SQL-Server 数据库,其中包含按 datetime2(2) 列分区的大型表。某些(旧)文件组标记为 READ_ONLY。

我定期使用 READ_WRITE_FILEGROUPS 选项进行备份。我可以成功地从读写分区恢复数据。

但是,我无法读取恢复的数据,出现以下错误:

表“dbo.myorders”的索引“pk_myorderid”的一个分区(分区 ID 72057594043105280)驻留在文件组(“YEAR2021”)上,该文件组因脱机、正在恢复或失效而无法访问。这可能会限制查询结果。

如果我将数据类型更改为 DATETIME 或 datetime2(7),则不会发生错误(当然,如果我从恢复的范围请求数据)

除了这个问题之外,其他一切都工作正常。

我创建了一个测试脚本来说明问题。该脚本创建一个测试数据库、填充表、备份和恢复数据库。

如果在此脚本中将 datetime2(7) 更改为 datetime2(2) ,则恢复后数据将变得无法访问。

测试脚本:

USE MASTER

-- Reset environment
IF DB_ID('PartialDatabase') IS NOT NULL
BEGIN
    EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'PartialDatabase'
    ALTER DATABASE PartialDatabase SET SINGLE_USER WITH ROLLBACK IMMEDIATE 
    DROP DATABASE PartialDatabase
END
GO
IF DB_ID('PartialDatabase_Recovery') IS NOT NULL
BEGIN
    EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'PartialDatabase_Recovery'
    ALTER DATABASE PartialDatabase_Recovery SET SINGLE_USER WITH ROLLBACK IMMEDIATE 
    DROP DATABASE PartialDatabase_Recovery
END
GO


-- Create database
CREATE DATABASE [PartialDatabase] ON PRIMARY (
    NAME = N'PartialDatabase'
  , FILENAME = N'C:\SQLData\PartialDatabase_primary.mdf'
  , SIZE = 10240KB , FILEGROWTH = 10240KB )
  
  , FILEGROUP [YEAR2021]
(   NAME = N'PartialDatabase_YEAR2021'
  , FILENAME = N'C:\SQLData\PartialDatabase_YEAR2021.ndf'
  , SIZE = 10240KB , FILEGROWTH = 10240KB )

  , FILEGROUP [YEAR2022]
(   NAME = N'PartialDatabase_YEAR2022'
  , FILENAME = N'C:\SQLData\PartialDatabase_YEAR2022.ndf'
  , SIZE = 10240KB , FILEGROWTH = 10240KB )

  , FILEGROUP [YEAR2023]
(   NAME = N'PartialDatabase_YEAR2023'
  , FILENAME = N'C:\SQLData\PartialDatabase_YEAR2023.ndf'
  , SIZE = 10240KB , FILEGROWTH = 10240KB ) 
 
  LOG ON
(   NAME = N'PartialDatabase_log'
  , FILENAME = N'C:\SQLData\PartialDatabase_log.ldf'
  , SIZE = 10240KB , FILEGROWTH = 10240KB )
GO

 
ALTER DATABASE [PartialDatabase] SET RECOVERY SIMPLE
GO
 
-- create partition FUNCTION & SCHEME
USE [PartialDatabase]
GO
CREATE PARTITION FUNCTION pf_myorders_date ([datetime2](7)) /*([datetime2](2))*/
     AS RANGE RIGHT FOR VALUES
    ('2022-01-01 00:00:00', 
    '2023-01-01 00:00:00')

CREATE PARTITION SCHEME ps_myorders_date AS PARTITION pf_myorders_date 
    TO ([YEAR2021], [YEAR2022],[YEAR2023])
GO
 
-- Create table
CREATE TABLE dbo.myorders
(
       myorder_id      INT                
     , myorder_date    [datetime2](7)                       /*([datetime2](2))*/
     , myorder_details NVARCHAR(4000)     
     , CONSTRAINT pk_myorderid PRIMARY KEY CLUSTERED (myorder_id, myorder_date)
)
ON ps_myorders_date(myorder_date)
GO

/*
Insert rows to all partitions
*/
INSERT INTO [PartialDatabase].dbo.myorders SELECT 1, '2020-01-01 10:00:00', 'year - 2020'
INSERT INTO [PartialDatabase].dbo.myorders SELECT 2, '2021-01-01 10:00:00', 'year - 2021'
INSERT INTO [PartialDatabase].dbo.myorders SELECT 3, '2022-01-01 10:00:00', 'year - 2022'
INSERT INTO [PartialDatabase].dbo.myorders SELECT 4, '2023-01-01 10:00:00', 'year - 2023'



GO


-- Mark old partitions as readonly
alter database [PartialDatabase] set SINGLE_USER with rollback immediate
GO

ALTER DATABASE [PartialDatabase] MODIFY FILEGROUP [YEAR2021] READONLY
ALTER DATABASE [PartialDatabase] MODIFY FILEGROUP [YEAR2022] READONLY

alter database [PartialDatabase] set MULTI_USER with rollback immediate
GO


-- Backup READ_WRITE filegroups
BACKUP DATABASE PartialDatabase 
READ_WRITE_FILEGROUPS
TO DISK = N'C:\SQLData\PartialDatabase_2023.bak'
WITH INIT, STATS = 10;
GO


--Restore READ_WRITE filegroups
RESTORE DATABASE [PartialDatabase_Recovery] 
READ_WRITE_FILEGROUPS
FROM DISK = N'C:\SQLData\PartialDatabase_2023.bak'
WITH PARTIAL, RECOVERY, 
MOVE 'PartialDatabase' TO 'C:\SQLData\PartialDatabase_Recovery_Primary.mdf',
MOVE 'PartialDatabase_YEAR2021' TO 'C:\SQLData\PartialDatabase_Recovery_YEAR2021.ndf',
MOVE 'PartialDatabase_YEAR2022' TO 'C:\SQLData\PartialDatabase_Recovery_YEAR2022.ndf',
MOVE 'PartialDatabase_YEAR2023' TO 'C:\SQLData\PartialDatabase_Recovery_YEAR2023.ndf',
MOVE 'PartialDatabase_log' TO 'C:\SQLData\PartialDatabase_Recovery_log.ldf'
GO

-- Request data located in the READ_WRITE filegroup 
SELECT [myorder_id]
      ,[myorder_date]
      ,[myorder_details]
  FROM [PartialDatabase_Recovery].[dbo].[myorders]
WHERE [myorder_date] >= '2023-01-01'  

Run Code Online (Sandbox Code Playgroud)

当然,使用 DATETIME 类型是一个可行的解决方案,但是 datetime2(2) 有什么问题呢?

我更愿意仅通过修改 SQL 数据库来解决问题,而不影响软件代码库。一些应用程序在实际查询中使用常量(而不是参数)。

我使用的是 SQL Server 2017 14.0.1000.169 (X64)。

Pau*_*ite 5

当数据类型不匹配并且存在截断风险时,您不会获得动态分区消除,正如我十年前在我的文章为什么\xe2\x80\x99t 分区消除有效?(见下文)。

\n

至于行为本身,我当时写道:

\n
\n

我\xe2\x80\x99m 有两种想法,即SQL Server 2008+ 的行为是错误、疏忽还是修复其他内容的不良后果\xe2\x80\xa6,所以我为其打开了一个连接项该报告已关闭为Won\xe2\x80\x99t Fix

\n
\n

关闭此错误的原因是错误中报告的场景不够常见,并且由于实施修复的风险,不幸的是它不符合产品当前版本的标准。

\n
\n
\n

解释

\n

您的查询:

\n
SELECT\n    M.myorder_id, \n    M.myorder_date, \n    M.myorder_details \nFROM dbo.myorders AS M\nWHERE \n    M.myorder_date >= \'2023-01-01\';\n
Run Code Online (Sandbox Code Playgroud)\n

正如所写,查询符合简单参数化的条件。字符串文字被赋予推断类型varchar(8000)(有关完整详细信息,请参阅我的文章系列“简单参数化和琐碎计划”),然后在运行时隐式转换datetime2(7)为 以便与列进行比较datetime(2)

\n

使用隐式转换进行扫描且不消除分区

\n

截断的风险意味着没有应用动态分区消除,RangePartitionNew如我的文章中所述。

\n

当查询不符合简单参数化的条件时,字符串仍会隐式转换为,datetime2(7)但这更难看到,因为应用了常量折叠而不是执行运行时转换(请注意末尾的七个小数位)。但由于存在截断风险,仍然没有消除分区:

\n
SELECT\n    M.myorder_id, \n    M.myorder_date, \n    M.myorder_details \nFROM dbo.myorders AS M\nWHERE \n    M.myorder_date >= \'2023-01-01\'\nOPTION (KEEP PLAN); -- hint prevents simple param\n
Run Code Online (Sandbox Code Playgroud)\n

使用隐式转换、常量折叠和无分区消除进行扫描

\n

如果我们使用参数或显式转换提供正确键入的值,则可以避免所有这些问题,并且可以进行分区消除,无论是否有简单的参数化:

\n
-- Simple param allowed\nSELECT\n    M.myorder_id, \n    M.myorder_date, \n    M.myorder_details \nFROM dbo.myorders AS M\nWHERE \n    M.myorder_date >= CONVERT(datetime2(2), \'2023-01-01\', 120)\n
Run Code Online (Sandbox Code Playgroud)\n

使用简单参数进行动态分区消除

\n

请注意此处应用动态消除的查找谓词。

\n

该计划更加简单,无需简单的参数化,因为在常量折叠后可以使用静态分区消除:

\n

静态分区消除

\n

注意文字中的两个小数位并直接查找正确的分区 ID。

\n

当然,任何避免访问脱机分区的计划都可以避免该错误。需要明确的是,问题不在于没有消除分区的计划不是最佳的,问题是这些计划必须访问不可用的分区。

\n

这就是为什么我强烈鼓励人们在编码时注意数据类型的原因之一。字符串不是日期/时间,潜在的问题有很多。

\n

解决方法

\n

许多解决方法都是可行的,但都需要某种改变。一个简单的方法是添加一个OPTION (RECOMPILE)一种简单的方法是直接或通过计划指南向查询

\n

这允许参数嵌入选项,其中文字常量折叠为正确的类型,从而消除静态分区。缺点是增加了编译并且没有计划重用。

\n

已确认在具有恢复挂起文件组的 SQL Server 2022 上有效:

\n
SELECT \n    DF.data_space_id,\n    DF.[type_desc],\n    DF.[name],\n    DF.state_desc\nFROM sys.database_files AS DF\nORDER BY\n    DF.data_space_id;\n
Run Code Online (Sandbox Code Playgroud)\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
数据空间id类型描述姓名状态描述
0日志测试日志在线的
1行列式测试在线的
2行列式测试_YEAR2021RECOVERY_PENDING
3行列式测试_YEAR2022RECOVERY_PENDING
4行列式测试_YEAR2023在线的
\n
\n
SELECT\n    M.myorder_id, \n    M.myorder_date, \n    M.myorder_details \nFROM dbo.myorders AS M\nWHERE \n    M.myorder_date >= \'2023-01-01\'\nOPTION (RECOMPILE);\n
Run Code Online (Sandbox Code Playgroud)\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
我的订单 ID我的订单日期我的订单详细信息
42023-01-01 10:00:00.00年份 - 2023
\n
\n

使用选项(重新编译)进行计划

\n
\n

另一种选择是使用以下命令明确正在访问的分区$PARTITION

\n
SELECT\n    M.myorder_id, \n    M.myorder_date, \n    M.myorder_details \nFROM dbo.myorders AS M\nWHERE \n    M.myorder_date >= \'2023-01-01\'\n    AND $PARTITION.pf_myorders_date(M.myorder_date) = \n        $PARTITION.pf_myorders_date(\'2023-01-01\');\n
Run Code Online (Sandbox Code Playgroud)\n

您可以将额外的逻辑包装在内联函数或视图中,并让用户访问这些逻辑而不是基表,甚至可以在另一个数据库中创建新对象的同义词,这样就不必更改查询。哪种创意选项最适合您取决于您​​的具体环境和工作限制。

\n

概括

\n

最终,访问具有某些脱机文件组的数据库有点偶然。这几乎是设计使然。假设逐步恢复最终将导致数据库完全恢复。如果尝试访问尚未恢复的文件组,则在该过程完成之前运行的查询可能会失败。

\n

如果你从来没有还原剩余的文件组,则需要做一些工作将部分还原的数据库转换为功能齐全的数据库。

\n

例如,您可以创建目标表的空分区副本,SWITCH将所需的文件组放入其中,重命名旧表,最后重命名新对象以匹配原始对象。这可能需要大量工作,尤其是在需要重新创建外键关系的情况下,但这是最全面的解决方案。具体如何执行此操作取决于您希望新表的外观(例如是否分区),但这是一个生成新分区表的示例脚本:

\n
USE Test_Recovery;\n\nCREATE PARTITION FUNCTION pf_myorders_date2 ([datetime2](2))\n    AS RANGE RIGHT \n    FOR VALUES\n    (\n        \'2023-01-01 00:00:00\'\n    );\n\nCREATE PARTITION SCHEME ps_myorders_date2 \n    AS PARTITION pf_myorders_date2 \n    TO ([PRIMARY], [YEAR2023]);\n\nIF OBJECT_ID(N\'dbo.myorders_defunct\', \'U\') IS NULL\nBEGIN TRY\n    BEGIN TRANSACTION;\n\n    -- Will become the new table\n    CREATE TABLE dbo.myorders_switch\n    (\n        myorder_id integer NOT NULL,\n        myorder_date datetime2(2) NOT NULL,\n        myorder_details nvarchar(4000) NULL, \n    \n        CONSTRAINT pk_myorderid_switch \n            PRIMARY KEY CLUSTERED (myorder_id, myorder_date)\n    )\n    ON ps_myorders_date2(myorder_date);\n\n    -- Move the data\n    ALTER TABLE dbo.myorders\n        SWITCH PARTITION 3\n        TO dbo.myorders_switch\n            PARTITION 2;\n\n    -- Rename the old table as defunct (cannot drop)\n    EXECUTE sys.sp_rename\n        @objname = N\'dbo.myorders\',\n        @newname = N\'myorders_defunct\',\n        @objtype = \'OBJECT\';\n\n    -- Rename the old primary key\n    EXECUTE sys.sp_rename\n        @objname = N\'pk_myorderid\',\n        @newname = N\'pk_myorderid_defunct\',\n        @objtype = \'OBJECT\';\n\n    -- Rename the switch table\n    EXECUTE sys.sp_rename\n        @objname = N\'dbo.myorders_switch\',\n        @newname = N\'myorders\',\n        @objtype = \'OBJECT\';\n\n    -- Rename the switch primary key\n    EXECUTE sys.sp_rename\n        @objname = N\'pk_myorderid_switch\',\n        @newname = N\'pk_myorderid\',\n        @objtype = \'OBJECT\';\n\n    COMMIT TRANSACTION;\nEND TRY\nBEGIN CATCH\n    IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;\n    THROW;\nEND CATCH;\n
Run Code Online (Sandbox Code Playgroud)\n

它并不完美,因为您最终仍然会得到RECOVERY_PENDING名为的文件组和空表*_defunct,但新表不会有任何原始问题。完美的结果需要创建一个新的数据库并批量复制行。

\n

顺便说一下,您使用的是 2017 RTM 版本。此后已累计更新 31 次。您可能想将其修补到最新。

\n