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)。
当数据类型不匹配并且存在截断风险时,您不会获得动态分区消除,正如我十年前在我的文章为什么\xe2\x80\x99t 分区消除有效?(见下文)。
\n至于行为本身,我当时写道:
\n\n\n我\xe2\x80\x99m 有两种想法,即SQL Server 2008+ 的行为是错误、疏忽还是修复其他内容的不良后果\xe2\x80\xa6,所以我为其打开了一个连接项。该报告已关闭为Won\xe2\x80\x99t Fix:
\n\n\n关闭此错误的原因是错误中报告的场景不够常见,并且由于实施修复的风险,不幸的是它不符合产品当前版本的标准。
\n
您的查询:
\nSELECT\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\';\nRun Code Online (Sandbox Code Playgroud)\n正如所写,查询符合简单参数化的条件。字符串文字被赋予推断类型varchar(8000)(有关完整详细信息,请参阅我的文章系列“简单参数化和琐碎计划”),然后在运行时隐式转换datetime2(7)为 以便与列进行比较datetime(2):
截断的风险意味着没有应用动态分区消除,RangePartitionNew如我的文章中所述。
当查询不符合简单参数化的条件时,字符串仍会隐式转换为,datetime2(7)但这更难看到,因为应用了常量折叠而不是执行运行时转换(请注意末尾的七个小数位)。但由于存在截断风险,仍然没有消除分区:
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\nRun 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)\nRun Code Online (Sandbox Code Playgroud)\n\n请注意此处应用动态消除的查找谓词。
\n该计划更加简单,无需简单的参数化,因为在常量折叠后可以使用静态分区消除:
\n\n注意文字中的两个小数位并直接查找正确的分区 ID。
\n当然,任何避免访问脱机分区的计划都可以避免该错误。需要明确的是,问题不在于没有消除分区的计划不是最佳的,问题是这些计划必须访问不可用的分区。
\n这就是为什么我强烈鼓励人们在编码时注意数据类型的原因之一。字符串不是日期/时间,潜在的问题有很多。
\n许多解决方法都是可行的,但都需要某种改变。一个简单的方法是添加一个OPTION (RECOMPILE)一种简单的方法是直接或通过计划指南向查询
这允许参数嵌入选项,其中文字常量折叠为正确的类型,从而消除静态分区。缺点是增加了编译并且没有计划重用。
\n已确认在具有恢复挂起文件组的 SQL Server 2022 上有效:
\nSELECT \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;\nRun Code Online (Sandbox Code Playgroud)\n| 数据空间id | 类型描述 | 姓名 | 状态描述 |
|---|---|---|---|
| 0 | 日志 | 测试日志 | 在线的 |
| 1 | 行列式 | 测试 | 在线的 |
| 2 | 行列式 | 测试_YEAR2021 | RECOVERY_PENDING |
| 3 | 行列式 | 测试_YEAR2022 | RECOVERY_PENDING |
| 4 | 行列式 | 测试_YEAR2023 | 在线的 |
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);\nRun Code Online (Sandbox Code Playgroud)\n| 我的订单 ID | 我的订单日期 | 我的订单详细信息 |
|---|---|---|
| 4 | 2023-01-01 10:00:00.00 | 年份 - 2023 |
另一种选择是使用以下命令明确正在访问的分区$PARTITION:
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\');\nRun Code Online (Sandbox Code Playgroud)\n您可以将额外的逻辑包装在内联函数或视图中,并让用户访问这些逻辑而不是基表,甚至可以在另一个数据库中创建新对象的同义词,这样就不必更改查询。哪种创意选项最适合您取决于您的具体环境和工作限制。
\n最终,访问具有某些脱机文件组的数据库有点偶然。这几乎是设计使然。假设逐步恢复最终将导致数据库完全恢复。如果尝试访问尚未恢复的文件组,则在该过程完成之前运行的查询可能会失败。
\n如果你从来没有还原剩余的文件组,则需要做一些工作将部分还原的数据库转换为功能齐全的数据库。
\n例如,您可以创建目标表的空分区副本,SWITCH将所需的文件组放入其中,重命名旧表,最后重命名新对象以匹配原始对象。这可能需要大量工作,尤其是在需要重新创建外键关系的情况下,但这是最全面的解决方案。具体如何执行此操作取决于您希望新表的外观(例如是否分区),但这是一个生成新分区表的示例脚本:
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;\nRun Code Online (Sandbox Code Playgroud)\n它并不完美,因为您最终仍然会得到RECOVERY_PENDING名为的文件组和空表*_defunct,但新表不会有任何原始问题。完美的结果需要创建一个新的数据库并批量复制行。
顺便说一下,您使用的是 2017 RTM 版本。此后已累计更新 31 次。您可能想将其修补到最新。
\n| 归档时间: |
|
| 查看次数: |
216 次 |
| 最近记录: |