【问题标题】:Safely normalizing data via SQL query通过 SQL 查询安全地规范化数据
【发布时间】:2010-11-02 12:57:42
【问题描述】:

假设我有一张客户表:

CREATE TABLE customers (
    customer_number  INTEGER,
    customer_name    VARCHAR(...),
    customer_address VARCHAR(...)
)

此表没有有主键。但是,customer_namecustomer_address 应该对于任何给定的 customer_number 都是唯一的。

此表包含许多重复客户的情况并不少见。为了避免这种重复,以下查询仅用于隔离唯一客户:

SELECT
  DISTINCT customer_number, customer_name, customer_address
FROM customers

幸运的是,该表格历来包含准确的数据。也就是说,对于任何customer_number,从来没有冲突的customer_namecustomer_address。但是,假设有冲突的数据确实进入了表中。我希望编写一个会失败的查询,而不是为有问题的customer_number 返回多行。

例如,我尝试了这个查询没有成功:

SELECT
  customer_number, DISTINCT(customer_name, customer_address)
FROM customers
GROUP BY customer_number

有没有办法使用标准 SQL 编写这样的查询?如果没有,在 Oracle 特定的 SQL 中是否有解决方案?

编辑:奇怪查询背后的基本原理:

说实话,这个客户表实际上并不存在(谢天谢地)。我创建了它,希望它足够清晰来展示查询的需求。但是,基于该示例,人们(幸运地)意识到对此类查询的需求是我最不担心的。因此,我现在必须剥离一些抽象概念,并希望恢复我提出这种可憎的桌子的声誉......

我从外部系统收到一个包含发票(每行一个)的平面文件。我逐行读取此文件,将其字段插入此表:

CREATE TABLE unprocessed_invoices (
    invoice_number   INTEGER,
    invoice_date     DATE,
    ...
    // other invoice columns
    ...
    customer_number  INTEGER,
    customer_name    VARCHAR(...),
    customer_address VARCHAR(...)
)

如您所见,来自外部系统的数据是非规范化的。也就是说,外部系统在同一行上同时包含发票数据及其关联的客户数据。多张发票可能会共享同一个客户,因此可能会有重复的客户数据。

在保证所有客户都已在系统中注册之前,系统无法开始处理发票。因此,系统必须识别唯一客户并在必要时对其进行注册。这就是我想要查询的原因:因为我正在处理我无法控制的非规范化数据

SELECT
  customer_number, DISTINCT(customer_name, customer_address)
FROM unprocessed_invoices
GROUP BY customer_number

希望这有助于澄清问题的初衷。

编辑:好/坏数据示例

澄清一下:customer_namecustomer_address 只需对于特定的 customer_number 是唯一的。

 customer_number | customer_name | customer_address
----------------------------------------------------
 1               | 'Bob'         | '123 Street'
 1               | 'Bob'         | '123 Street'
 2               | 'Bob'         | '123 Street'
 2               | 'Bob'         | '123 Street'
 3               | 'Fred'        | '456 Avenue'
 3               | 'Fred'        | '789 Crescent'

前两行没问题,因为customer_number 1 的customer_namecustomer_address 相同。

中间两行没问题,因为customer_number 2 的customer_namecustomer_address 相同(即使另一个customer_number 具有相同的customer_namecustomer_address)。

最后两行不行,因为customer_number 3 有两个不同的customer_addresses。

如果针对所有六行运行,我正在查找的查询将失败。但是,如果实际只存在前四行,则视图应该返回:

 customer_number | customer_name | customer_address
----------------------------------------------------
 1               | 'Bob'         | '123 Street'
 2               | 'Bob'         | '123 Street'

我希望这能澄清我所说的“customer_namecustomer_address 冲突”的意思。根据customer_number,它们必须是唯一的。

感谢那些解释如何从外部系统正确导入数据的人。事实上,我已经在做大部分工作了。我故意隐藏了我正在做的所有细节,以便更容易专注于手头的问题。此查询并不是唯一的验证形式。我只是认为这会是一个很好的画龙点睛(可以说是最后的防守)。这个问题只是为了调查 SQL 的可能性。 :)

【问题讨论】:

  • “失败,而不是返回多行”是什么意思?通常当我想到一个 sql 查询失败时,这意味着我没有行,或者我正在加入的表的产品。我虽然您正在寻找 select * from (select count(*) as cnt, customer_number, customer_name, customer_address From customers group by customer_number, customer_name, customer_address) where cnt > 1 type of query.
  • “失败”是指 DBMS 应该返回错误而不是结果(例如当您查询不存在的表时)。我知道我可以使用“SELECT COUNT(*) ... GROUP BY ... HAVING ...”查询来识别冲突数据的存在,但是我只是想看看是否有类似于我的查询描述的确实存在。谢谢你的评论,不过! :)

标签: sql denormalization


【解决方案1】:
Select t1.* from #temp t1
join #temp t2 
  on t1.customer_name = t2.customer_name and t1.customer_address = t2.customer_address 
where t1.customer_number <> t2.customer_number

select t1.* from #temp t1
join 
(select customer_number from #temp group by customer_number having count(*) >1) t2
  on t1.customer_number = t2.customer_number

【讨论】:

    【解决方案2】:

    让我们使用不同的查询将数据放入临时表或表变量中

    select distinct customer_number, customer_name, customer_address, 
      IDENTITY(int, 1,1) AS ID_Num
    into #temp 
    from unprocessed_invoices
    

    如果可能的话,我个人也会在未处理的发票上添加一个不完整的信息。我从来没有在不创建具有标识列的临时表的情况下进行导入,只是因为删除重复记录更容易。

    现在让我们查询该表以查找您的问题记录。我想你会想看看是什么导致了问题,而不仅仅是让他们失望。

    Select t1.* from #temp t1
    join #temp t2 
      on t1.customer_name = t2.customer_name and t1.customer_address = t2.customer_address 
    where t1.customer_number <> t2.customer_number
    
    select t1.* from #temp t1
    join 
    (select customer_number from #temp group by customer_number having count(*) >1) t2
      on t1.customer_number = t2.customer_number
    

    您可以使用这些查询的变体从#temp 中删除问题记录(取决于您是选择保留一个还是删除所有可能的问题),然后从#temp 插入到您的生产表中。您还可以将问题记录提供给为您提供数据的任何人,以便他们最终修复。

    【讨论】:

      【解决方案3】:

      如果您希望它失败,您将需要一个索引。如果你不想有索引,那么你可以创建一个临时表来完成这一切。

      CREATE TABLE #temp_customers 
          (customer_number int, 
          customer_name varchar(50), 
          customer_address varchar(50),
          PRIMARY KEY (customer_number),
           UNIQUE(customr_name, customer_address))
      

      )

      INSERT INTO #temp_customers
      SELECT DISTINCT customer_number, customer_name, customer_address
      FROM customers
      
      SELECT customer_number, customer_name, customer_address
      FROM #temp_customers
      
      DROP TABLE #temp_customers
      

      如果出现问题,这将失败,但会防止您的重复记录引起问题。

      【讨论】:

      • 如果我有 (1, Bob, 1st Street) 和 (2, Bob, 1st Street)...这两个都返回。
      • 抱歉,我没有想到那种情况。我现在添加了一个 UNIQUE 约束。现在应该可以了。
      【解决方案4】:

      你的方法有缺陷。您不希望成功存储的数据在选择时引发错误 - 这是等待发生的地雷,意味着您永远不知道选择何时会失败。

      我建议您向表中添加一个唯一键,然后慢慢开始修改您的应用程序以使用此键,而不是依赖任何有意义的数据组合。

      然后,您就可以停止关心重复数据,这些数据一开始并不是真正的重复。完全有可能两个同名的人共享同一个地址。

      您还将通过这种方法获得性能改进。

      顺便说一句,我强烈建议您对数据进行规范化,即将名称分解为 FirstName 和 LastName(也可以选择 MiddleName),并将地址字段分解为每个组件的单独字段(Address1、Address2、City、州、国家/地区、邮编或其他)

      更新:如果我正确理解了您的情况(我不确定我是否正确),您希望防止表格中出现重复的姓名和地址组合(即使这是现实生活中可能发生)。这最好通过这两个字段上的唯一约束或索引来完成,以防止插入数据。也就是说,在插入之前捕获错误。这将告诉您导入文件或您生成的应用程序逻辑错误,然后您可以选择采取适当的措施。

      我仍然认为,当您查询时抛出错误在游戏中为时已晚,无法对此采取任何措施。

      【讨论】:

      • +1 任何严肃的数据表都应该有一个主键来唯一标识每一个数据行——数据库设计 101!
      • 你们俩都完全正确。我希望能在没有人质疑其设计的情况下回答这个问题。 ;) 我希望我的最新编辑可以帮助阐明这个问题。
      【解决方案5】:

      标量子查询只能返回一行(每个结果集行...),因此您可以执行以下操作:

      选择不同的 顾客号码, ( 选择不同的 客户地址 来自客户 c2 其中 c2.customer_number = c.customer_number ) 作为客户地址 来自客户 c

      【讨论】:

      • +1。这实际上是我最初尝试的。但是,我希望有一种方法可以避免为每个不同的列编写子查询。不过谢谢!
      【解决方案6】:

      如果你有脏数据,我会先清理它。

      使用它来查找重复的客户记录...

      Select * From customers
      Where customer_number in 
        (Select Customer_number from customers
        Group by customer_number Having count(*) > 1)
      

      【讨论】:

        【解决方案7】:

        事实上的键是名称+地址,所以这就是你需要分组的。

        SELECT
          Customer_Name,
          Customer_Address,
          CASE WHEN Count(DISTINCT Customer_Number) > 1
            THEN 1/0 ELSE 0 END as LandMine
        FROM Customers
        GROUP BY Customer_Name, Customer_Address
        

        如果您想从 Customer_Number 的角度来做,那么这也很好。

        SELECT *, 
        CASE WHEN Exists((
          SELECT top 1 1
          FROM Customers c2
          WHERE c1.Customer_Number != c2.Customer_Number
            AND c1.Customer_Name = c2.Customer_Name
            AND c1.Customer_Address = c2.Customer_Address
        )) THEN 1/0 ELSE 0 END as LandMine
        FROM Customers c1
        WHERE Customer_Number = @Number
        

        【讨论】:

          【解决方案8】:

          使查询失败可能很棘手...

          这将显示表中是否有任何重复记录:

          select customer_number, customer_name, customer_address
          from customers
          group by customer_number, customer_name, customer_address
          having count(*) > 1
          

          如果只是为所有三个字段添加唯一索引,则没有人可以在表中创建重复记录。

          【讨论】:

          • 添加到 Guffa 的帖子中,尝试添加 if 语句,检查计数,如果计数 > 1 则使用 RAISEERROR
          猜你喜欢
          • 1970-01-01
          • 2014-10-21
          • 2017-05-09
          • 1970-01-01
          • 1970-01-01
          • 2016-12-28
          • 1970-01-01
          • 2018-01-20
          • 2011-09-19
          相关资源
          最近更新 更多