Безопасная нормализация данных с помощью SQL-запроса

Допустим, у меня есть таблица клиентов:

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? Если нет, то есть ли решение в SQL-запросе Oracle?

РЕДАКТИРОВАТЬ. Причина странного запроса:

По правде говоря, этой таблицы клиентов на самом деле не существует (слава богу). Я создал его в надежде, что он будет достаточно ясным, чтобы продемонстрировать потребности запроса. Однако люди (к счастью) понимают, что необходимость в таком запросе - меньшая из моих проблем, основываясь на этом примере. Поэтому теперь я должен избавиться от некоторой абстракции и, надеюсь, восстановить свою репутацию за то, что предлагал такую ​​мерзость стола ...

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

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_addresses для customer_number 3.

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

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

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

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


person Adam Paynter    schedule 12.06.2009    source источник
comment
Что вы подразумеваете под ошибкой, а не под возвратом нескольких строк? Обычно, когда я думаю о сбое запроса sql, это означает, что у меня нет строк или продукта таблиц, к которым я присоединяюсь. Я думал, вы искали select * из (select count (*) as cnt, customer_number, customer_name, customer_address From customers group by customer_number, customer_name, customer_address), где cnt ›1 тип запроса.   -  person Charlie    schedule 12.06.2009
comment
Под ошибкой я имел в виду, что СУБД должна возвращать ошибку, а не результаты (например, когда вы запрашиваете несуществующую таблицу). Я понимаю, что могу использовать запрос SELECT COUNT (*) ... GROUP BY ... HAVING ... для определения наличия конфликтующих данных, однако мне было просто любопытно посмотреть, действительно ли запрос, похожий на тот, который я описал существовал. Тем не менее, спасибо за комментарий! :)   -  person Adam Paynter    schedule 12.06.2009


Ответы (7)


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

select distinct
       customer_number,
       (
       select distinct
              customer_address
         from customers c2
        where c2.customer_number = c.customer_number
       ) as customer_address
  from customers c
person Scott Swank    schedule 12.06.2009
comment
+1. На самом деле это то, что я изначально пытался. Однако я надеялся найти способ, позволяющий избежать написания подзапроса для каждого отдельного столбца. Спасибо хоть! - person Adam Paynter; 12.06.2009

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

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

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

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

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

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

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

person D'Arcy Rittich    schedule 12.06.2009
comment
+1 любая серьезная таблица данных обязательно должна иметь первичный ключ для однозначной идентификации каждой отдельной строки данных - дизайн базы данных 101! - person marc_s; 12.06.2009
comment
Вы оба абсолютно правы. Я надеялся, что на этот вопрос будет дан ответ, и люди не будут сомневаться в его конструкции. ;) Надеюсь, моя последняя редакция поможет пролить свет на этот вопрос. - person Adam Paynter; 12.06.2009

Сбой запроса может быть сложной задачей ...

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

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

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

person Guffa    schedule 12.06.2009
comment
Добавляя сообщение Гуффы, попробуйте добавить оператор if, проверьте счетчик и используйте RAISEERROR, если счетчик ›1 - person Saif Khan; 12.06.2009

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

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
person Amy B    schedule 12.06.2009

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

Используйте это, чтобы найти повторяющиеся записи о клиентах ...

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

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

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

Это не удастся, если есть проблемы, но ваши дублирующие записи не будут вызывать проблем.

person Justin Balvanz    schedule 12.06.2009
comment
Если у меня есть (1, Боб, 1-я улица) и (2, Боб, 1-я улица) ... это возвращает оба. - person Amy B; 12.06.2009
comment
Извините, я не подумал об этом сценарии. Я добавил УНИКАЛЬНОЕ ограничение. Теперь это должно сработать. - person Justin Balvanz; 12.06.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 в рабочую таблицу. Вы также можете передать записи о проблемах тому, кто предоставляет вам данные, которые будут исправлены на их стороне.

person HLGEM    schedule 12.06.2009