Безопасная нормализация данных с помощью SQL-запроса - PullRequest
4 голосов
/ 12 июня 2009

Предположим, у меня есть таблица клиентов:

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

Эта таблица не имеет первичный ключ. Однако customer_name и customer_address должны быть уникальными для любого заданного customer_number.

В этой таблице нередко встречается много повторяющихся клиентов. Чтобы обойти это дублирование, следующий запрос используется для изоляции только уникальных клиентов:

SELECT
  DISTINCT customer_number, customer_name, customer_address
FROM customers

К счастью, таблица традиционно содержит точные данные. То есть никогда не было конфликтующих customer_name или customer_address для любого customer_number. Однако предположим, что противоречивые данные попали в таблицу. Я хочу написать запрос, который не будет выполнен, вместо того, чтобы возвращать несколько строк для рассматриваемого 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_name и customer_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_name и customer_address для customer_number 1.

Средние две строки в порядке, потому что они одинаковы customer_name и customer_address для customer_number 2 (даже если у другого customer_number есть такие же customer_name и customer_address).

Последние две строки не в порядке , потому что есть два разных customer_address es для customer_number 3.

Запрос, который я ищу, не будет выполнен, если он будет выполнен для всех шести из этих строк. Однако, если в действительности существуют только первые четыре строки, представление должно вернуть:

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

Надеюсь, это проясняет, что я имел в виду под "конфликтующими customer_name и customer_address". Они должны быть уникальными для customer_number.

Я ценю тех, кто объясняет, как правильно импортировать данные из внешних систем. На самом деле, я уже делаю большую часть этого уже. Я нарочно скрыл все детали того, что я делаю, чтобы было легче сосредоточиться на данном вопросе. Этот запрос не является единственной формой проверки. Я просто думал, что это будет хорошим завершающим штрихом (последняя защита, так сказать). Этот вопрос был просто разработан, чтобы исследовать только то, что было возможно с SQL. :)

Ответы [ 8 ]

3 голосов
/ 12 июня 2009

Ваш подход несовершенен. Вы не хотите, чтобы данные, которые были успешно сохранены, могли затем вызвать ошибку выбора - это наземная мина, которая должна произойти и означает, что вы никогда не знаете, когда выбор может быть неудачным.

Что я рекомендую, так это добавить уникальный ключ в таблицу и медленно начать модифицировать свое приложение, чтобы использовать этот ключ, а не полагаться на какую-либо комбинацию значимых данных.

Вы можете перестать заботиться о дублирующих данных, которые на самом деле не являются дубликатами. Два человека с одинаковыми именами могут использовать один и тот же адрес.

Вы также получите улучшение производительности от этого подхода.

Кроме того, я настоятельно рекомендую вам нормализовать ваши данные, то есть разбить имя на FirstName и LastName (опционально также MiddleName) и разбить поле адреса на отдельные поля для каждого компонента (Address1, Address2, City, Штат, Страна, Почтовый индекс или что-то еще)

Обновление: Если я правильно понимаю вашу ситуацию (что я не уверен, что понимаю), вы хотите, чтобы в таблице никогда не возникало повторяющихся комбинаций имени и адреса (даже если это возможно появление в реальной жизни). Это лучше всего сделать с помощью уникального ограничения или индекса для этих двух полей, чтобы предотвратить вставку данных. То есть перехватите ошибку до того, как вставите ее. Это скажет вам, что файл импорта или полученная логика приложения неверны, и тогда вы сможете принять соответствующие меры.

Я по-прежнему утверждаю, что выдает ошибку, когда вы запрашиваете в игре слишком поздно, чтобы что-то с этим сделать.

2 голосов
/ 12 июня 2009

Скалярный подзапрос должен возвращать только одну строку (на строку набора результатов ...), чтобы вы могли сделать что-то вроде:

select distinct
       customer_number,
       (
       select distinct
              customer_address
         from customers c2
        where c2.customer_number = c.customer_number
       ) as customer_address
  from customers c
0 голосов
/ 05 января 2016
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
0 голосов
/ 13 июня 2009

Давайте поместим данные во временную таблицу или табличную переменную с вашим отдельным запросом

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 в рабочую таблицу. Вы также можете передать записи о проблемах тому, кто предоставит вам данные для исправления в их конце.

0 голосов
/ 12 июня 2009

Если у вас есть грязные данные, я бы сначала их очистил.

Используйте это, чтобы найти дубликаты записей о клиентах ...

Select * From customers
Where customer_number in 
  (Select Customer_number from customers
  Group by customer_number Having count(*) > 1)
0 голосов
/ 12 июня 2009

Если вы хотите, чтобы он потерпел неудачу, вам понадобится индекс. Если вы не хотите иметь индекс, то вы можете просто создать временную таблицу, чтобы сделать все это.

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

Это не удастся, если возникнут проблемы, но не допустит возникновения дубликатов ваших записей.

0 голосов
/ 12 июня 2009

Ключ defacto - это Имя + Адрес, так что это то, что вам нужно сгруппировать.

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
0 голосов
/ 12 июня 2009

Ошибка выполнения запроса может быть сложной ...

Это покажет вам, есть ли в таблице повторяющиеся записи:

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

Если вы просто добавите уникальный индекс для всех трех полей, никто не сможет создать дублирующую запись в таблице.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...