如何为 SQL 表自动生成数据类型

Posted

技术标签:

【中文标题】如何为 SQL 表自动生成数据类型【英文标题】:How to auto generate data types for a SQL table 【发布时间】:2020-09-05 03:41:01 【问题描述】:

我有许多包含 200 多列的非规范化表。这些表位于 SQL Server 中,并且它们通常(如果不总是)具有 varchar(100) 或 nvarchar(100)(字符串)数据类型。但是,大多数列是整数、浮点数或其他数据类型。我不可能遍历每个表和列并挑选数据类型。由于许多原因,包括应用程序兼容性、性能、加入和其他原因,我必须将这些转换为正确的(或至少接近正确的)数据类型。有什么我可以使用的工具吗?有没有人创建代码来完成这个?它不一定是完美的,但接近匹配就可以了。

我尝试了什么:

    我尝试将这些表导出到 Excel,然后返回 SQL。它起作用了,但是比手动操作要花费更多的时间,因为 Excel 会破坏您的数据并将其转换为任何感觉(想想科学记数法、数字日期等……天哪!)。这非常耗时并且失败了。如果您选择在 Excel 中使用“文本”选项,它只会将所有内容转换回 varchar (x) 我尝试导出到平面文件并使用具有智能数据类型的 VS 或 SSMS 新版本。这比 Excel 效果更好,但不幸的是,即使是单行冲突也会停止整个过程。该工具很笨重,会出现严重错误,并且不会告诉您是哪一行导致了问题。使用这种方法也很糟糕,因为这些表很大,而且非常耗时。尤其是在考虑工具故障排除时。

感谢您的帮助。如果您不要求我通过尝试说我的设置不好/等等来放弃任务,我也很感激。

【问题讨论】:

您能否澄清一下,当您说“正确”数据类型时,“正确”是什么意思? @Nabav 我认为这很清楚,但我会更明确。如果 A 列的值介于 1-20 之间,理想情况下它应该是一个小的 int 或 int。具有 2.4 的 colB 应该是浮点数,而不是 varchar(100)。我明白你不可能有完美的结果。我也了解潜在的危险,例如带有前导 0 的邮政编码会丢失,但至少拥有合理正确的数据类型是一个好的开始。 我做了something like this for integer-based data。但是,如果您的起始列是一个字符串,我想您将从任何一个即使单个值是 ISNUMERIC() = 0 的列开始,您也可以消除转换为数字类型的可能性,并且任何具有一个 ISDATE() = 0 的值,您从日期/时间候选人中删除。如果您有所有数据为 ISNUMERIC 的列,那么您可以开始测试它们的长度(最大长度、小数点后的最大长度等)。 @AaronBertrand 谢谢!您的文章对整数非常有价值,可惜它不适用于字符串!实际上,我认为使用 ISNUMERIC/ISDATE 确实是我应该使用的。我知道这些功能,但不知何故,在想到这个可怕的任务时从未想到过!再次感谢 我在写答案时没有想到这一点,但我突然想到TRY_CONVERT() 可能是一种更精确的方法来确定一个值是否应该是某种类型,而不是依靠 ISNUMERIC/ISDATE 的牛仔滑稽动作(它们不是超级可靠的)。 CASE WHEN TRY_CONVERT(tinyint, col) IS NULL THEN 'will not fit in tintint' WHEN TRY_CONVERT(int, col) IS NULL THEN 'will not fit in int' etc. etc. 【参考方案1】:

我现在假设您只关心以下字符串列:

    应保留为字符串,但定义的宽度可能超出所需范围 不应该是字符串,因为: 它们只包含日期 它们只包含数字,在这种情况下: 您将关心长度(以确定潜在的 tinyint/int/bigint) 你会关心它们是否包含小数

您已经看到了一种确定if columns already defined as integers could be made smaller 的方法,但类似的方法可用于在数据类型当前为字符串但符合上述条件之一的表中查找潜在候选者。

假设你有一张这样的桌子:

CREATE TABLE dbo.foo
(
  a int PRIMARY KEY, 
  h varchar(100),
  i varchar(100),
  j varchar(100)
);

INSERT dbo.foo VALUES 
(1,'123','123','20200101 04:00:00'),
(2,'456','456','20200101'),
(3,'789','789','20200101'),
(4,'867','foo','20200101'),
(5,'876','876','20200101'),
(6,'6.54','654','20200101');

一种方法是确定列的所有元数据按照它们的定义(您可以从sys.dm_exec_describe_first_result_set 轻松获得),然后从该构建动态 SQL 中检查每一列的最长的值(这将确定最小的字符串大小),是否有单个非数字(这意味着您无法转换为数字),是否有单个非日期(这意味着您无法转换为日期),以及是否有小数点(这意味着您不能转换为 int 系列,但您还需要检查精度/比例)。

这绝对只​​是一个粗鲁、肮脏的开始,但它应该能让你继续前进。

DECLARE @table nvarchar(513) = N'dbo.foo';

DECLARE @sql nvarchar(max) = N'SELECT ', @un nvarchar(max) = N'',
  @un_sub nvarchar(max) = N'
  SELECT ColumnName =  MIN([col $c$]), 
  CurrentType = MIN([type $c$]), 
  LongestValue = MAX([len $c$]), 
  [AllNumerics?] = MIN([is_num $c$]), 
  [AllDates?] = MIN([is_date $c$]),
  [AnyContainDecimal] = MAX([has_dec $c$]) FROM x '

SELECT @sql += N'[col ' + name + '] = ''' + name + ''',
   [type ' + name + '] = '''
  + system_type_name + ''',' + QUOTENAME(name)
  + ', [len ' + name + '] = LEN(' + QUOTENAME(name) + '),
  [is_num ' + name + '] = CONVERT(tinyint, ISNUMERIC(' + QUOTENAME(name) + ')),
  [is_date ' + name + '] = CONVERT(tinyint, ISDATE(' + QUOTENAME(name) + ')),
  [has_dec ' + name + '] = CASE WHEN ISNUMERIC(' + QUOTENAME(name) + ') = 1
    AND ' + QUOTENAME(name) + ' LIKE N''%.%'' THEN 1 ELSE 0 END,',
  @un += N'
UNION ALL ' + REPLACE(@un_sub, N'$c$', name)
  
FROM sys.dm_exec_describe_first_result_set('SELECT * FROM ' + @table, NULL, 1)
WHERE system_type_name like '%char%'

SELECT @sql += N'[$garbage$]='''' FROM ' + @table;

SELECT @sql = N';WITH x AS (
' + @sql + N'
) ' + STUFF(@un, 1, 10, '');

EXEC sys.sp_executesql @sql;

要消化的东西很多……动态 SQL 很强大,但它真的很丑,而且不完全面向对象。

结果(try it out in this fiddle):

你可以在那里看到:

    h全是数字,最长为4,但至少有一个值包含小数点,所以这里的最优类型是decimal(something, something)i 至少包含一个非数字,至少一个非日期,因此只能是字符串,但由于最长的值只有 3 个字符,varchar(100) 太多了。无论您可以转到 varchar(3)char(3) 还是需要通过一些填充来保证未来的发展,这实际上只是一个您可以根据您的数据模型、现在和以后的业务需求等定性回答的问题。 j 包含所有日期类型,但是您不能从这里的最大长度解释太多(因为您不知道日期实际上是如何存储的,因为它们存储为字符串和许多形式的字符串可以解释为有效日期)。因此,您可能知道j 应该是某种风格的datetime,但您需要仔细查看值以了解实际存在的内容。

您可以将此查询的结果(尤其是对于具有大量列的表)更改为仅返回值得研究的值,在这种情况下,我返回所有行以进行演示(无论如何,所有行在我的示例中都有潜在的修复) .只需在联合周围添加另一个 CTE 并根据这些列(或您添加的其他列)进行过滤。

当然,在大表上,这可能会扫描每一列,所以不要指望它很快,如果你缺少它,它会不喜欢很多内存。同样,这可能很明显,但这不能保护您免于选择以后会伤害您的类型。假设该列正在收集整数并且它刚刚达到 99,因此您将类型更改为 tinyint,因为没有小数并且最长长度为 2。然后有人插入 256 并繁荣。

您还可以添加其他增强功能,例如获取最小长度(如果它们都是字符串,也许您有 varchar 但它可能是 char),或者检查是否有任何字符在 ASCII 之外(也许你有nvarchar,但它可能是varchar)、小数点两边的位数(更精确地表示十进制类型)或最大值(以提高确定整数类型的准确性)。我会把这些留作练习。

【讨论】:

天哪,我没想到我的问题会有两个如此严肃的答案!非常感谢,我真的希望我能投票给他们,但我没有足够的代表,我希望我能选择两个最佳答案!我真的两个都用过。非常感谢您提供如此有用的答案!【参考方案2】:

您可能在 SQL Server 中有一个更简单的解决方案。只需尝试转换值并选择最合适的类型。对于单列处理整数、日期和时间非常简单:

select (case when count(try_convert(tinyint, col)) = count(col) then 'tinyint'
             when count(try_convert(int, col)) = count(col) then 'int'
             when count(try_convert(bigint, col)) = count(col) then 'bigint'
             when count(try_convert(date, col)) = count(col) then 'date'
             when count(try_convert(time, col)) = count(col) then 'time'
             when count(try_convert(datetime, col)) = count(col) then 'datetime'
             else 'varchar(255)'  -- or whatever default
        end)
from t
where col is not null;

这需要以两种方式扩展。一个用于更多列,第二个用于其他类型的数字。第一个很简单:

select colname,
       (case when count(try_convert(tinyint, col)) = count(col) then 'tinyint'
             when count(try_convert(int, col)) = count(col) then 'int'
             when count(try_convert(bigint, col)) = count(col) then 'bigint'
             when count(try_convert(date, col)) = count(col) then 'date'
             when count(try_convert(time, col)) = count(col) then 'time'
             when count(try_convert(datetime, col)) = count(col) then 'datetime'
             else 'varchar(255)'  -- or whatever default
        end)
from t cross apply
     (values ('col1', col1), ('col2', col2), . . . ) v(colname, col)
where col is not null
group by colname;

注意:如果值都是NULL,则无法检查。

带小数点的数字的问题是值不明确——您想要数字还是浮点数?一种可能性是您考虑到了数据类型。所以,您可能知道所有数字都可能是,所以numeric(20, 4) 因为它们代表货币金额——您可以将它们包括在上面。

或者您可以测试小数位的位置并使用该信息来推导类型。我认为最简单的解决方案可能是这样的:

select colname,
       (case when count(try_convert(tinyint, col)) = count(col) then 'tinyint'
             when count(try_convert(int, col)) = count(col) then 'int'
             when count(try_convert(bigint, col)) = count(col) then 'bigint'
             when count(try_convert(date, col)) = count(col) then 'date'
             when count(try_convert(time, col)) = count(col) then 'time'
             when count(try_convert(datetime, col)) = count(col) then 'datetime'
             when count(try_convert(numeric(20, 4), col)) = count(col) and
                  sum(case when col like '%._____' then 1 else 0 end) = 0
             then 'numeric(20, 4)'
             when count(try_convert(float, col) = count(col)
             then 'float'
             else 'varchar(255)'  -- or whatever default
        end)
from t cross apply
     (values ('col1', col1), ('col2', col2), . . . ) v(colname, col)
where col is not null
group by colname;

【讨论】:

max(try_) 将始终在存在可以转换/转换的单行时返回结果,无论是否无法转换所有其他行。查询可以使用 count() 而不是 max():count(try_convert(xyzdatatype, col)) = count(col) @liptr 。 . .这是一个有趣的观点。我将更改逻辑以处理 1 次失败。 @GordonLinoff 我从没想过我的问题会有两个如此严肃的答案!非常感谢,我真的希望我能投票给他们,但我没有足够的代表,我希望我能选择两个最佳答案!我真的两个都用过。非常感谢您提供如此有用的答案!【参考方案3】:

你可以考虑在表格上使用一个视图,其中视图使用一个带有转换函数的select

select someFunction(colA), someOtherFunction(colB) ... from tableName

例如对于sqlserver

CREATE VIEW myView
as
select CAST(colA AS int) as colA, CAST(colB AS text) as colB ... 
from tableName

然后你可以说 select ... from myView

【讨论】:

感谢您的回答。但我不确定这会有什么帮助。您正在手动转换每个 col。编写这段代码是我试图避免的。如果我必须手动创建语句并猜测数据类型,这将是一周的手动工作,这就是问题所在。 除非您可以让计算机读取您的想法 - 您必须指定要转换为的类型。如果您希望将所有列转换为相同的类型,或者使用某种算法 - 您可以编写一个程序来为视图编写 SQL 查询,并让这个 SQL 编写器程序为您创建 VIEW 语句并创建视图。您甚至可以使用 Excel 来编写视图查询。将元数据(列名、数据类型等)放入 Excel 并让 Excel 为视图创建 SQL。您可以将数据库中的表元数据导出到 Excel 以开始使用。 阅读想法?这真的只是数据采样,我没有想到。

以上是关于如何为 SQL 表自动生成数据类型的主要内容,如果未能解决你的问题,请参考以下文章

在 SQL Server 中,如何为给定表生成 CREATE TABLE 语句?

如何为每个商店自动增加特定的数据库表值。 (Laravel 6)

在sql创建数据库表时,如何为字段设一个默认值

如何为数据库表中的每条记录生成一个id?

角度材料数据表 - 如何为具有提前类型/自动完成搜索的列设置 filterPredicate?

在反应状态引擎中,如何为结果表的每一行生成唯一的 UUID?