【问题标题】:Calculating age from a varchar column with discrepancies从有差异的 varchar 列计算年龄
【发布时间】:2018-04-03 19:21:09
【问题描述】:

希望您度过了一个宁静的复活节。感谢您是否可以在以下方面为我提供建议/帮助。 (使用函数/不使用函数)

下面是我的数据集,期望的输出(使用规则中的 DOB 规范导出年龄)

需要您的帮助(请注意,我在 MSSQL 环境中寻找解决方案):-
1. 提出年龄字段。(我尝试了以下脚本,但它没有工作,因为它不够动态,无法包含所有 DOB 规则,我还附上了一个 oracle 脚本,它可以作为供大家参考)

SELECT 
[ID],
[DOB],
 'age' = DATEDIFF(HOUR,(CONVERT(date,(CASE WHEN ([DOB] like '99/%/%') THEN (REPLACE([DOB],'99','01'))
               ELSE [DOB] END),103)),GETDATE())/8766 
from [Sample]

Sample_Dataset

create table Sample (
  Id  Varchar (50),
  DOB Varchar (50))

  insert into Sample(Id, DOB)
  Values 
  ('38603', '24/02/1969'),
  ('38605', '22/09/1969'),
  ('36356', '17/03/1954'),
  ('36374', '17/05/1975'),
  ('36441', '17/08/1961'),
  ('1a', '10/05/9999'),
  ('1b', '10/99/9999'),
  ('1c', '99/99/9999'),
  ('2a', '--/--/1935'),
  ('2b', '00/00/1935'),
  ('2c', '88/88/1935'),
  ('2d', '99/99/1935'),
  ('3a', '10/--/1935'),
  ('3b', '10/00/1935'),
  ('3c', '10/88/1935'),
  ('3d', '10/99/1935'),
  ('4a', '--/09/1935'),
  ('4b', '00/09/1935'),
  ('4c', '88/09/1935'),
  ('4d', '99/09/1935')

期望的输出

身份证 |出生日期 |年龄(截至 05-03-2018; dd-mm-yyyy) 38603 | 1969 年 2 月 24 日 | 49——一切皆知 38605 | 22/09/1969 | 48 36356 | 1954 年 3 月 17 日 | 63 36374 | 1975 年 5 月 17 日 | 42 36441 | 1961 年 8 月 17 日 | 56 1a | 10/05/9999 |null -- 未知年份 1b | 10/99/9999 |空 1c | 99/99/9999 |空 2a | --/--/1935 |82 --未知的日期和月份 2b | 00/00/1935 |82 2c | 1935 年 88 月 88 日 |82 二维 | 99/99/1935 |82 3a | 10/---/1935 |82 --未知月份但已知年份 3b | 1935 年 10 月 00 日 |82 3c | 1935 年 10 月 88 日 |82 3d | 1935 年 10 月 99 日 |82 4a | --/09/1935 |82 --未知日期但已知月份 4b | 00/09/1935 |82 4c | 1935 年 9 月 88 日 |82 4d | 1935 年 9 月 99 日 |82

规则:- 正如您在 cmets 中的上述 5 个场景 中看到的那样

  1. 一切都是已知的(使用规定的出生日期计算年龄)
  2. 未知的年份(将年龄设为null,因为年份是已知的)
  3. 未知的日期和月份(使用 01/07 表示未知的 dd/mm 和声明的 yyyy)
  4. 未知的月份但已知的日期(使用 07 表示未知的 mm 和规定的 dd/07/yyyy)
  5. 未知的日期但已知的月份(使用 15 表示未知的 dd 和规定的 15/mm/yyyy)

Oracle 中的解决方案

首先创建一个函数(尝试在T-SQL中复制这个逻辑但不成功,因此我在这里)

create or replace function check_dt(in_date in VARCHAR2, in_format in VARCHAR2 default 'DD/MM/YYYY')
RETURN NUMBER
IS
V_DATE DATE;
V_STATUS INTEGER;
BEGIN

 SELECT TO_DATE(in_date,in_format)
 INTO V_DATECASE  
 FROM DUAL;

 V_STATUS := 0;
 RETURN V_STATUS;  
 EXCEPTION WHEN OTHERS THEN
 V_STATUS := SQLCODE; 
         RETURN V_STATUS;
        END;

        select check_dt('11/30/2017') from dual;
        select TO_DATE('15/--/9999','DD/MM/YYYY') from dual;

select id, dob,
       case when check_dt(dob) = -1843 --not valid month, default it to July (07)
               THEN substr(dob,1,2)||'/07'||substr(dob,7,4) 
            when check_dt(dob) = -01847 -- day of month must between 1 and last day of month
               THEN '1/07/'||substr(dob,7,4)
            WHEN check_dt(dob) = 0 and to_date(dob,'dd/mm/yyyy') > sysdate
               THEN NULL
            WHEN check_dt(dob) = -0183 -- date not valid for month
               THEN '15/'||substr(dob,4)
            ELSE
               THEN dob 
            END New_dob
from SAMPLE; 

任何帮助将不胜感激。 非常感谢。

【问题讨论】:

  • mysql sql-servermysql 不使用 tsql。你能相应地更新你的标签吗?
  • @Larnu:感谢您的关注。我在想mssql。没有正确阅读。再次感谢。

标签: sql sql-server database tsql


【解决方案1】:

SQL 服务器

SELECT id,
       CASE WHEN YEAR(GETDATE())-REVERSE(LEFT(REVERSE(DOB), CHARINDEX('/', REVERSE(DOB)) - 1)) > = 0 
            THEN 
              YEAR(GETDATE())-REVERSE(LEFT(REVERSE(DOB), CHARINDEX('/', REVERSE(DOB)) - 1))
          ELSE 
             NULL
       END AS Age
FROM Sample

您的问题的解决方案

WITH CTE AS
(
 SELECT id,
       CASE WHEN ISNUMERIC(REVERSE(LEFT(REVERSE(DOB), CHARINDEX('/', REVERSE(DOB)) - 1))) = 1  THEN
                REVERSE(LEFT(REVERSE(DOB), CHARINDEX('/', REVERSE(DOB)) - 1))
            ELSE
                NULL
        END
       AS Year,
       CASE WHEN ISNUMERIC(LEFT(DOB, CHARINDEX('/', DOB) - 1)) = 1 THEN
                 LEFT(DOB, CHARINDEX('/', DOB) - 1)
            ELSE
                NULL
       END AS DAY,
       CASE WHEN ISNUMERIC(SUBSTRING(DOB,CHARINDEX('/',DOB)+1, CHARINDEX('/',DOB,CHARINDEX('/',DOB)+1) -CHARINDEX('/',DOB)-1)) = 1 THEN
            CASE WHEN SUBSTRING(DOB,CHARINDEX('/',DOB)+1, CHARINDEX('/',DOB,CHARINDEX('/',DOB)+1) -CHARINDEX('/',DOB)-1) >= 1 AND SUBSTRING(DOB,CHARINDEX('/',DOB)+1, CHARINDEX('/',DOB,CHARINDEX('/',DOB)+1) -CHARINDEX('/',DOB)-1)  <= 12 THEN
                SUBSTRING(DOB,CHARINDEX('/',DOB)+1, CHARINDEX('/',DOB,CHARINDEX('/',DOB)+1) -CHARINDEX('/',DOB)-1)
                ELSE 
                NULL
            END
            ELSE
                NULL
        END AS MONTH
FROM Sample),CTE1 AS
(
  SELECT id,
         year,
         month,
         CASE WHEN DAY IS NOT NULL THEN
              CASE WHEN DAY >= 1 AND DAY <= DAY(EOMONTH(year+'-'+month+'-01')) THEN
                DAY
              ELSE
                NULL
              END  
         ELSE NULL
         END AS Day
  FROM CTE
)
,CTE2 AS
(
SELECT id,
           CASE WHEN YEAR IS NULL
                       THEN NULL
                     ELSE
                       CASE WHEN DAY IS NULL AND MONTH IS NULL THEN '01/07'
                            WHEN MONTH IS NULL AND DAY IS NOT NULL THEN CAST(day AS VARCHAR)+'/07'
                            WHEN MONTH IS NOT NULL AND DAY IS NULL THEN '15/'+CAST(MONTH AS VARCHAR)
                            ELSE CAST(day AS VARCHAR)+'/'+CAST(MONTH AS VARCHAR)
                       END
                       + '/'+CAST(YEAR AS VARCHAR)
                END
        AS DOB
FROM CTE1
)
SELECT id,DOB,
   CASE WHEN DOB IS NOT NULL
        THEN 
          CASE WHEN DATEDIFF (day,  CONVERT(DATE, DOB, 103),CONVERT(DATE,GETDATE(),103)) >=0
           THEN FLOOR(DATEDIFF (day, CONVERT(DATE, DOB, 103), CONVERT(DATE,GETDATE(),103)) / 365.2425)
           ELSE
              NULL
          END
      ELSE 
         DOB
   END AS Age
FROM CTE2

现场演示

http://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=3714d33cacb02c3fce4f0868c9d0990b

【讨论】:

  • 您好,这并没有给出如上所述的所需年龄。它简单地计算 2018-yyyy。
  • 检查第二个解决方案
  • Shankar 年龄只考虑年份。对于 ID=38605 | DOB= 22/09/1969 |希望的年龄应该是 48 岁,因为生日还没有过去。感谢您的尝试
  • 立即查看@Stephanie
  • 我认为 /365.2425 在 (ID=36356, DOB='17/03/1954') 上不起作用,因为它是在 3 月 17 日而不是第一个。我可以知道你如何找出每个月使用哪个分母吗?谢谢。
【解决方案2】:

您可以使用以下内容。我使用不同的 CTE 向您展示了从您的 varchar DOB 获取出生日期的过程。我还把桌子换成了临时的。

 IF OBJECT_ID('tempdb..#Sample') IS NOT NULL
    DROP TABLE #Sample

create table #Sample (
  Id Int,
  DOB Varchar (50))

insert into #Sample(Id, DOB)
Values 
(38603, '24/02/1969'),
(38605, '22/09/1969'),
(36356, '17/03/1954'),
(36374, '17/05/1975'),
(36441, '17/08/1961'),
(119, '10/05/9999'),
(114, '10/99/9999'),
(132, '99/99/9999'),
(25125, '--/--/1935'),
(2323, '00/00/1935'),
(2512, '88/88/1935'),
(2156, '99/99/1935'),
(368, '10/--/1935'),
(34135, '10/00/1935'),
(3435, '10/88/1935'),
(3241, '10/99/1935'),
(4512, '--/09/1935'),
(4161, '00/09/1935'),
(4312, '88/09/1935'),
(456, '99/09/1935')

;WITH ParsedBirth AS
(
    SELECT
        S.Id,
        S.DOB,
        Year = SUBSTRING(S.DOB, 7, 4),
        Month = SUBSTRING(S.DOB, 4, 2),
        Day = SUBSTRING(S.DOB, 1, 2)
    FROM
        #Sample AS S
),
ParsedBirthInteger AS
(
    SELECT
        P.Id,
        P.DOB,
        Year = CASE WHEN ISNUMERIC(P.Year) = 1 AND P.Year <> '9999' THEN CONVERT(INT, P.Year) END,
        Month = CASE 
            WHEN ISNUMERIC(P.Month) = 1 AND CONVERT(INT, P.Month) BETWEEN 1 AND 12 THEN CONVERT(INT, P.Month) 
            ELSE 7 END,
        Day = CASE 
            WHEN ISNUMERIC(P.Day) = 1 AND CONVERT(INT, P.Day) BETWEEN 1 AND 31 THEN CONVERT(INT, P.Day) 
            ELSE 15 END
    FROM
        ParsedBirth AS P
),
InferredBirth AS
(
    SELECT
        P.Id,
        P.DOB,
        InferredBirth = CONVERT(DATE, CONVERT(VARCHAR(100), P.Year * 10000 + P.Month * 100 + P.Day))
    FROM
        ParsedBirthInteger AS P
)
SELECT
    T.Id,
    T.DOB,
    T.InferredBirth,
    Age = (CONVERT(INT,CONVERT(char(8), GETDATE(),112))-CONVERT(char(8),T.InferredBirth,112))/10000
FROM
    InferredBirth AS T

【讨论】:

  • 这个解决方案与我的非常相似,但在细节上使用了一些不同的方法。来自我这边的 +1 和一个小小的提示:这依赖于像 dd/MM/yyyy 这样的固定格式,并且会打破像 1/12/200012/1/2000 这样的值。但是这里给出的例子似乎很严格......
  • @Ezequiel López Petrucci:你好。感谢您再次尝试帮助我。什么是 CTE?并且#sample也是mssql中的一个虚拟表,就像在oracle中的dual一样?
  • @Ezequiel:年龄只考虑年份。对于 ID=38605 | DOB= 22/09/1969 |希望的年龄应该是 48 岁,因为生日还没有过去。感谢您的尝试。
  • @Stephanie 哦,是的,我更正了年龄计算。 CTE 是公用表表达式及其用“WITH”声明的“子查询”(只是一种避免嵌套子查询的形式,以及其他用途)。以 # 开头的表称为临时表,并在会话终止(断开连接)时自动删除。在 SQL Server 中,“对偶”不存在,在这种情况下,您可以省略 SQL Server 中的 FROM 子句(如SELECT 999 AS Number)。
  • @EzequielLópezPetrucci:早上好,谢谢您的解释。我认为它在 (ID=36356, DOB='17/03/1954') 上不起作用,因为它是在 3 月 17 日而不是第一次。感谢您提供帮助。
【解决方案3】:

首先:

  • 以文化相关的字符串格式存储日期是一个非常糟糕的主意。
  • 使用 magic values 是一个非常糟糕的主意(9999 表示“没有年份”)。
  • 混合这个是最危险、最糟糕的主意!

以下代码会将您的值转换为实际存储时应使用的格式。您可以从这里构建您的年龄逻辑,但我真的建议您使用这种方法来清理这些混乱并正确存储您的数据!

DECLARE @sample TABLE(
  Id VARCHAR(10),
  DOB VARCHAR (50))

  INSERT INTO @sample(Id, DOB)
  VALUES 
  ('38603', '24/02/1969'),
  ('38605', '22/09/1969'),
  ('36356', '17/03/1954'),
  ('36374', '17/05/1975'),
  ('36441', '17/08/1961'),
  ('1a', '10/05/9999'),
  ('1b', '10/99/9999'),
  ('1c', '99/99/9999'),
  ('2a', '--/--/1935'),
  ('2b', '00/00/1935'),
  ('2c', '88/88/1935'),
  ('2d', '99/99/1935'),
  ('3a', '10/--/1935'),
  ('3b', '10/00/1935'),
  ('3c', '10/88/1935'),
  ('3d', '10/99/1935'),
  ('4a', '--/09/1935'),
  ('4b', '00/09/1935'),
  ('4c', '88/09/1935'),
  ('4d', '99/09/1935');

--查询将在/ 上拆分您的字符串并尝试将值转换为int

WITH Splitted AS
(
    SELECT Id
          ,DOB 
          ,CAST('<x>' + REPLACE(DOB,'/','</x><x>') + '</x>' AS XML).value('/x[1]','varchar(10)') AS DOB_Day
          ,CAST('<x>' + REPLACE(DOB,'/','</x><x>') + '</x>' AS XML).value('/x[2]','varchar(10)') AS DOB_Month
          ,CAST('<x>' + REPLACE(DOB,'/','</x><x>') + '</x>' AS XML).value('/x[3]','varchar(10)') AS DOB_Year
    FROM @sample
)
,Casted AS
(
    SELECT Id
          ,DOB
           --below SQL-Server 2012 you can use `CASE` with `ISNUMERIC` instead of TRY_CAST
          ,TRY_CAST(DOB_Day AS INT)  AS CastedDay 
          ,TRY_CAST(DOB_Month AS INT)  AS CastedMonth
          ,TRY_CAST(DOB_Year AS INT)  AS CastedYear 
    FROM Splitted
)
,Checked AS
(
    SELECT Id
          ,DOB
          --You can use further logic to get the month's days correctly (instead of the plain 31)  
          ,CASE WHEN CastedDay BETWEEN 1 AND 31 THEN CastedDay ELSE NULL END AS TheDay
          ,CASE WHEN CastedMonth BETWEEN 1 AND 12 THEN CastedMonth ELSE NULL END AS TheMonth
          ,CASE WHEN CastedYear BETWEEN 1900 AND 2100 THEN CastedYear ELSE NULL END AS TheYear
    FROM Casted
)
SELECT *
FROM Checked; 

结果

+-------+------------+--------+----------+---------+
| Id    | DOB        | TheDay | TheMonth | TheYear |
+-------+------------+--------+----------+---------+
| 38603 | 24/02/1969 | 24     | 2        | 1969    |
+-------+------------+--------+----------+---------+
| 38605 | 22/09/1969 | 22     | 9        | 1969    |
+-------+------------+--------+----------+---------+
| 36356 | 17/03/1954 | 17     | 3        | 1954    |
+-------+------------+--------+----------+---------+
| 36374 | 17/05/1975 | 17     | 5        | 1975    |
+-------+------------+--------+----------+---------+
| 36441 | 17/08/1961 | 17     | 8        | 1961    |
+-------+------------+--------+----------+---------+
| 1a    | 10/05/9999 | 10     | 5        | NULL    |
+-------+------------+--------+----------+---------+
| 1b    | 10/99/9999 | 10     | NULL     | NULL    |
+-------+------------+--------+----------+---------+
| 1c    | 99/99/9999 | NULL   | NULL     | NULL    |
+-------+------------+--------+----------+---------+
| 2a    | --/--/1935 | NULL   | NULL     | 1935    |
+-------+------------+--------+----------+---------+
| 2b    | 00/00/1935 | NULL   | NULL     | 1935    |
+-------+------------+--------+----------+---------+
| 2c    | 88/88/1935 | NULL   | NULL     | 1935    |
+-------+------------+--------+----------+---------+
| 2d    | 99/99/1935 | NULL   | NULL     | 1935    |
+-------+------------+--------+----------+---------+
| 3a    | 10/--/1935 | 10     | NULL     | 1935    |
+-------+------------+--------+----------+---------+
| 3b    | 10/00/1935 | 10     | NULL     | 1935    |
+-------+------------+--------+----------+---------+
| 3c    | 10/88/1935 | 10     | NULL     | 1935    |
+-------+------------+--------+----------+---------+
| 3d    | 10/99/1935 | 10     | NULL     | 1935    |
+-------+------------+--------+----------+---------+
| 4a    | --/09/1935 | NULL   | 9        | 1935    |
+-------+------------+--------+----------+---------+
| 4b    | 00/09/1935 | NULL   | 9        | 1935    |
+-------+------------+--------+----------+---------+
| 4c    | 88/09/1935 | NULL   | 9        | 1935    |
+-------+------------+--------+----------+---------+
| 4d    | 99/09/1935 | NULL   | 9        | 1935    |
+-------+------------+--------+----------+---------+

【讨论】:

  • 了解这个坏主意,但如果它是遗留数据该怎么办。因此,它失控了。不更改源文件的原始数据是一种做法,因为它是收集的数据,尤其是在研究等行业中。但是,是的,谢谢你的提醒。这种方法工作量太大。不过谢谢。
  • @Stephanie 这怎么可能工作太多?您不能将此解释为日期。因此,您必须 1) 拆分字符串以分别获取值,2) 检查值是否可以转换为 int,然后 3) 检查值是否在给定范围内(针对 magic values)。没有更快的方法......不要更改原始数据,而是使用上述方法额外存储有效信息。在您(从某处)导入此数据时使用它,在 insert/update trigger 内或作为 UDF 来填充持久计算列。你不应该根据你的原始数据计算......
  • 是的,我同意,当它是脏数据时。在将数据操作为所需值之前,必须首先采取步骤清理数据。谢谢。
猜你喜欢
  • 1970-01-01
  • 2019-04-17
  • 2023-03-29
  • 1970-01-01
  • 1970-01-01
  • 2022-07-11
  • 2015-04-02
  • 2021-10-31
  • 2012-03-26
相关资源
最近更新 更多