Нормализовать таблицу, в которой необходимо ссылаться на подмножества столбца в другой таблице, и эти подмножества должны быть уникальными - PullRequest
0 голосов
/ 19 апреля 2020

Как мне нормализовать это отношение (то есть, чтобы оно соответствовало 1NF, 2NF и 3NF )

CREATE TABLE IF NOT EXISTS series (
  series_id SERIAL PRIMARY KEY,
  dimension_ids INT[] UNIQUE,
  dataset_id INT REFERENCES dataset(dataset_id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS dimension (
  dimension_id SERIAL PRIMARY KEY,
  dim VARCHAR(50),
  val VARCHAR(50),
  dataset_id INT REFERENCES dataset(dataset_id) ON DELETE CASCADE,
  UNIQUE (dim, val, dataset_id)
);

Где подмножества dimension_id уникально идентифицируют записи в series таблица.

РЕДАКТИРОВАТЬ

Для получения дополнительной информации я хочу сохранить данные из XML структур, похожих на следующие

<?xml version="1.0" encoding="utf-8"?>
<message:StructureSpecificData >
   <message:Header>
      <message:ID>IREF757740</message:ID>
      <message:Test>false</message:Test>
      <message:Prepared>2020-04-09T14:55:23</message:Prepared>
   </message:Header>
   <message:DataSet ss:dataScope="DataStructure" ss:structureRef="CPI" xsi:type="ns1:DataSetType">
      <Series FREQ="M" GEOG_AREA="WC" UNIT="IDX">
         <Obs OBS_STATUS="A" OBS_VALUE="75.5" TIME_PERIOD="31-Jan-2008"/>
         <Obs OBS_STATUS="A" OBS_VALUE="75.8" TIME_PERIOD="29-Feb-2008"/>
         <Obs OBS_STATUS="A" OBS_VALUE="77" TIME_PERIOD="31-Mar-2008"/>
         <Obs OBS_STATUS="A" OBS_VALUE="77.5" TIME_PERIOD="30-Apr-2008"/>
         <Obs OBS_STATUS="A" OBS_VALUE="78" TIME_PERIOD="31-May-2008"/>
         <Obs OBS_STATUS="A" OBS_VALUE="78.8" TIME_PERIOD="30-Jun-2008"/>
      </Series>
      <Series FREQ="M" GEOG_AREA="NC" UNIT="IDX">
         <Obs OBS_STATUS="A" OBS_VALUE="75.5" TIME_PERIOD="31-Jan-2008"/>
         <Obs OBS_STATUS="A" OBS_VALUE="75.8" TIME_PERIOD="29-Feb-2008"/>
         <Obs OBS_STATUS="A" OBS_VALUE="77" TIME_PERIOD="31-Mar-2008"/>
         <Obs OBS_STATUS="A" OBS_VALUE="77.5" TIME_PERIOD="30-Apr-2008"/>
         <Obs OBS_STATUS="A" OBS_VALUE="78" TIME_PERIOD="31-May-2008"/>
         <Obs OBS_STATUS="A" OBS_VALUE="78.8" TIME_PERIOD="30-Jun-2008"/>
      </Series>
   </message:DataSet>
</message:StructureSpecificData>

Там это набор данных, который содержит series (0 ... n), который содержит наблюдения (0 ... n). Ряды уникально идентифицируются по их атрибутам XML - что я называю измерениями в моей модели данных. В моем примере у меня есть два series, дифференцированных по географическим областям, которые они охватывают. Любой series может иметь произвольное количество измерений. Ожидается, что series будет запрашиваться из его размеров, а размеры будут также запрашиваться с помощью series_id. Очевидным решением является таблица мостов:

CREATE TABLE series_dimension
  series_id INT REFERENCES series(series_id) ON DELETE CASCADE,
  dimension_id INT REFERENCES dimension(dimension_id)
);

Это решение, однако, допускает следующий сценарий:

|--------------------------|
| series_dimension         |
|-----------|--------------|
| series_id | dimension_id |
|-----------|--------------|
| 1         | 1            |
| 1         | 2            |
| 1         | 3            |
| 1         | 4            |
| 2         | 1            |
| 2         | 2            |
| 2         | 3            |
| 2         | 4            |
|-----------|--------------|

То есть два разных series с одинаковыми размерами, поэтому что если я запрашиваю series для данного набора измерений, я не могу решить в случае измерений [1 2 3 4], ищу ли я series_id = 1 или series_id = 2, что неприемлемо. Поэтому в такой ситуации я должен выбрать между наличием ссылочной целостности и свойством уникальность , которое я только что объяснил?

Ответы [ 2 ]

0 голосов
/ 29 апреля 2020

Учитывая ваше ожидание около 20 размеров, пример ограничен 60. Требуется контролируемый процесс для определения каждого набора измерений (серии).


Обоснование

-- DIM is a valid numeric identifier for a dimension.
--
valid_dim {DIM}
       PK {DIM}

CHECK ((DIM = 1) OR ((DIM > 1) AND (mod(DIM,2) = 0)))


-- data sample
  (DIM)
---------
  (2^0)
, (2^1)
, (2^2)
, ...
, (2^58)
, (2^59)
-- Dimension DIM, named DIM_NAME exists.
--
dimension {DIM, DIM_NAME}
       PK {DIM}
       AK {DIM_NAME}

FK {DIM} REFERENCES valid_dim {DIM}


-- data sample
(DIM, DIM_NAME)
---------------
  (2^0, 'FREQ')
, (2^1, 'GEOG_AREA')
, (2^2, 'UNIT')
, ...
, (2^58, 'AGE_GROUP')
, (2^59, 'HAIR_COLOR')

Загрузка series и ser_dim может быть сделано из функции, приложения или чего-либо еще. Однако это должен быть контролируемый процесс.
SER уникален для данного набора измерений. Обратите внимание, что | является побитовым OR оператором.

-- Series SER, named SER_NAME exists.
--
series {SER, SER_NAME}
    PK {SER}
    AK {SER_NAME}


-- data sample
(SER, SER_NAME)
--------------------------------
  ((2^0 | 2^1 | 2^2)  , 'F-G-U')
, ((2^1 | 2^58)       , 'G-A'  )
, ((2^0 | 2^58 | 2^59), 'F-A-H')
-- Series SER has dimension DIM.
--
ser_dim {SER, DIM}
     PK {SER, DIM}

FK1 {SER} REFERENCES series    {SER}
FK2 {DIM} REFERENCES dimension {DIM}

CHECK ((DIM & SER) = DIM)

-- data sample
(SER, DIM)
--------------------------------
  ((2^0 | 2^1 | 2^2) , 2^0)
, ((2^0 | 2^1 | 2^2) , 2^1)
, ((2^0 | 2^1 | 2^2) , 2^2)

, ((2^1 | 2^58) , 2^1 )
, ((2^1 | 2^58) , 2^58)

, ((2^0 | 2^58 | 2^59), 2^0)
, ((2^0 | 2^58 | 2^59), 2^58)
, ((2^0 | 2^58 | 2^59), 2^59)

Примечание:

All attributes (columns) NOT NULL

PK = Primary Key
AK = Alternate Key (Unique)
FK = Foreign Key

PostgreSQL

-- DIM is a valid numeric identifier
-- for a dimension.
--
CREATE TABLE valid_dim (
      DIM bigint NOT NULL

    , CONSTRAINT pk_valid_dim PRIMARY KEY (DIM)

    , CONSTRAINT chk_valid_dim
        CHECK ( (DIM = 1)
                OR ( (DIM > 1)
                     AND (mod(DIM, 2) = 0) )
              )
);

-- define some of valid DIMs
INSERT INTO valid_dim (DIM)
VALUES
  ((2^ 0)::bigint)
, ((2^ 1)::bigint)
, ((2^ 2)::bigint)
-- fill this gap
, ((2^58)::bigint)
, ((2^59)::bigint) ;
-- Dimension DIM, named DIM_NAME exists.
--
CREATE TABLE dimension (
      DIM      bigint NOT NULL
    , DIM_NAME text   NOT NULL

    , CONSTRAINT pk_dim PRIMARY KEY (DIM)
    , CONSTRAINT ak_dim UNIQUE (DIM_NAME)

    , CONSTRAINT
        fk_dim   FOREIGN KEY (DIM)
        REFERENCES valid_dim (DIM)
);

-- define few dimensions
INSERT INTO dimension (DIM, DIM_NAME)
VALUES
  ((2^ 0)::bigint, 'FREQ')
, ((2^ 1)::bigint, 'GEOG_AREA')
, ((2^ 2)::bigint, 'UNIT')
, ((2^58)::bigint, 'AGE_GROUP')
, ((2^59)::bigint, 'HAIR_COLOR') ;
-- Series SER, named SER_NAME exists.
--
CREATE TABLE series (
     SER      bigint NOT NULL
   , SER_NAME text   NOT NULL

   , CONSTRAINT pk_series PRIMARY KEY (SER)
   , CONSTRAINT ak_series UNIQUE (SER_NAME)
);

-- define three series
INSERT INTO series (SER, SER_NAME)

SELECT bit_or(DIM) as SER, 'F-G-U' as SER_NAME
FROM dimension
WHERE DIM_NAME IN ('FREQ', 'GEOG_AREA', 'UNIT')

UNION

SELECT bit_or(DIM) as SER, 'G-A' as SER_NAME
FROM dimension
WHERE DIM_NAME IN ('GEOG_AREA', 'AGE_GROUP')

UNION

SELECT bit_or(DIM) as SER, 'F-A-H' as SER_NAME
FROM dimension
WHERE DIM_NAME IN ('FREQ', 'AGE_GROUP', 'HAIR_COLOR') ;
-- Series SER has dimension DIM.
--
CREATE TABLE ser_dim (
     SER bigint NOT NULL
   , DIM bigint NOT NULL

   , CONSTRAINT pk_ser_dim PRIMARY KEY (SER, DIM)

   , CONSTRAINT
       fk1_ser_dim FOREIGN KEY (SER)
             REFERENCES series (SER)

   , CONSTRAINT
       fk2_ser_dim FOREIGN KEY (DIM)
          REFERENCES dimension (DIM)

   , CONSTRAINT
        chk_ser_dim CHECK ((DIM & SER) = DIM)
);

-- populate ser_dim
INSERT INTO ser_dim (SER, DIM)
SELECT SER, DIM
FROM series
JOIN dimension ON true
WHERE (DIM & SER) = DIM ;

Другой вариант - использовать (материализованное) представление для ser_dim. Это зависит от остальной части модели: если FK необходим для {SER, DIM} хранения таблицы, в противном случае представление будет лучше.

-- An option, instead of the table.
--
CREATE VIEW ser_dim
AS
SELECT SER, DIM
FROM series
JOIN dimension ON true
WHERE (DIM & SER) = DIM ;

Test

-- Show already defined series
-- and their dimensions.
SELECT SER_NAME, DIM_NAME
FROM ser_dim  
JOIN series    USING (SER)
JOIN dimension USING (DIM)
ORDER BY SER_NAME, DIM_NAME ;
-- Get SER for a set of dimensions;
-- use this when defining a series.
SELECT bit_or(DIM) AS SER
FROM dimension
WHERE DIM_NAME IN ('FREQ', 'GEOG_AREA', 'UNIT') ;
-- Find already defined series,
-- given a set of dimensions.
SELECT x.SER
FROM (
  SELECT bit_or(DIM) AS SER
  FROM dimension
  WHERE DIM_NAME IN ('FREQ', 'GEOG_AREA', 'UNIT')
  ) AS x
WHERE EXISTS
  (SELECT 1 FROM series AS s WHERE s.SER = x.SER) ;

Резюме

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

0 голосов
/ 23 апреля 2020

Мой вывод, что это отношение (где столбец ссылается на атрибуты, число которых заранее неизвестно) требует, чтобы нормализация привела к созданию отношения «многие ко многим» или «один ко многим», и это исключает уникальное сопоставление ,

И наоборот, для отношения, где столбец ссылается на атрибуты, число которых заранее неизвестно, способ сделать отношение один к одному / уникальным состоит в том, чтобы сгруппировать эти атрибуты в уникальные подмножества, что нарушает 1NF .

  1. Существует только один способ указать UNIQUE ограничений, а именно для столбцов (столбцов)
  2. В моем примере требуется, чтобы каждый series_id ссылался номер переменной столбцов / размеров
  3. Поэтому я складываю столбцы в строки, в результате чего конструкция UNIQUE недоступна
  4. Решение имеет каждый series_id относятся к массиву, который определяет подмножества строк, теперь я могу указать, что этот столбец массивов равен UNIQUE
  5. Это нарушает 1NF , поэтому это отношение не может быть нормализовано
...