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

How to auto generate data types for a SQL table

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

我尝试了什么:

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

感谢您的帮助。如果您不通过尝试说我的设置是 bad/etc.

来让我放弃任务,我也很感激

您可以考虑在 table 上使用视图,其中视图使用 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 ... 来自 myView

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

  1. 应该保留为字符串,但可能定义得比需要的更宽
  2. 不应该是字符串,因为:
    • 它们只包含日期
    • 它们只包含数字,在这种情况下:
      • 你会关心长度(以确定潜在的tinyint/int/bigint)
      • 你会关心它们是否包含小数

您已经看到了一种确定 if columns already defined as integers could be made smaller 的方法,但是可以使用类似的方法在 table 中找到潜在的候选者,其中数据类型当前是字符串,但它满足以上标准。

假设您有这样一个 table:

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检查每一列的最长值(这将确定最小的字符串大小),是否有单个non-numeric(这意味着你不能转换为数字),是否有single non-date(表示不能转为date),是否有小数点(表示不能转为int family,但是还要检查precision/scale).

这绝对只是一个粗糙、肮脏的 kick-start,但它应该让你继续。

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很强大,但它真的很丑而且不完全object-oriented。

结果(try it out in this fiddle):

你可以看到:

  1. h都是数字,最长的值是4,但至少有一个值包含小数点,所以这里的最优类型是decimal(something, something).
  2. i至少包含一个non-numeric,也至少包含一个non-date,因此只能是一个字符串,但由于最长的值只有3个字符,varchar(100) 太多了。您是否可以转到 varchar(3)char(3) 或您需要 future-proof 稍加填充,这实际上只是一个您可以根据您的数据模型、现在和以后的业务需求定性回答的问题,等等
  3. j 包含所有日期类型,但是你不能从这里的最大长度中解释太多(因为你不知道日期实际是如何存储的,因为它们存储为字符串和许多字符串, 许多形式都可以解释为有效日期)。因此,您可能知道 j 应该是某种风格的 datetime,但您需要仔细观察值以了解实际存在的内容。

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

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

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

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

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;