Табличное представление данных из нормализованной базы данных SQL


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

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

A tracker таблица:

 id |      name       
----+---------------------
  1 | Grocery deliveries 
  2 | Sports cars

An entries таблица:

 id |      datetime       | tracker_id 
----+---------------------+------------
  1 | 2013-10-01 00:00:00 |          1
  2 | 2013-10-02 00:00:00 |          1
  3 | 2013-10-03 00:00:00 |          1

A values таблица:

 id | number | entry_id | category_id 
----+--------+----------+-------------
  1 |   10.0 |        1 |          1
  3 |   20.0 |        1 |          2
  5 |   21.0 |        1 |          3
  7 |   18.0 |        2 |          2
  8 |    4.0 |        3 |          1
  9 |    9.0 |        3 |          2

И таблица category:

 id |     name        | tracker_id 
----+-----------------+------------
  1 | Tomatoes        |          1
  2 | Carrots         |          1
  3 | Brussel sprouts |          1
  4 | Ferraris        |          2

Я хотел бы распечатать таблицу для трекера 1, с каждой строкой, соответствующей дате (никаких повторяющихся дат). Столбцы будут следующими: дата, категория 1 (Помидоры), категория 2 (Морковь), категория 3 (брюссельская капуста). Если на заданную дату не было значения для данной категории, оно будет пустым или будет отображаться как null. Так что, в идеале, это будет выглядеть что-то вроде этого:

 datetime            | Tomatoes | Carrots | Brussel sprouts
---------------------+----------+---------+-----------------
 2013-10-01 00:00:00 | 10.0     | 20.0    | 21.0
 2013-10-02 00:00:00 | Null     | 18.0    | Null
 2013-10-03 00:00:00 | 4.0      | Null    | 9.0
Я не знаю, как это сделать, и есть ли лучший способ хранения этих данных. Есть предложения?

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

2 2

2 ответа:

Вот как я бы определил ваши таблицы:

deliveries
    id          unsigned int(P)
    good_id     unsigned int(F goods.id)
    qwhen       datetime
    quantity    double

+----+---------+------------+----------+
| id | good_id | qwhen      | quantity |
+----+---------+------------+----------+
|  1 |       1 | 2013-10-01 |     10.0 |
|  2 |       2 | 2013-10-01 |     20.0 |
|  3 |       3 | 2013-10-01 |     21.0 |
|  4 |       2 | 2013-10-02 |     18.0 |
|  5 |       1 | 2013-10-03 |      4.0 |
|  6 |       2 | 2013-10-03 |      9.0 |
|  7 |       1 | 2013-10-01 |      3.0 |
| .. | ....... | ...........| ........ |
+----+---------+------------+----------+

good_types
    id                  unsigned int(P)
    name                varchar(50)

+----+-------------+
| id | name        |
+----+-------------+
|  1 | Groceries   |
|  2 | Sports cars |
+----+-------------+

goods
    id              unsigned int(P)
    good_type_id    unsigned int(F good_types.id)
    name            varchar(50)

+----+--------------+-----------------+
| id | good_type_id | name            |
+----+--------------+-----------------+
|  1 |            1 | Tomatoes        |
|  2 |            1 | Carrots         |
|  3 |            1 | Brussel Sprouts |
|  4 |            2 | Ferraris        |
| .. | ............ | ............... |
+----+--------------+-----------------+

И вот SQL, чтобы получить имена столбцов:

SELECT id, name
FROM goods
WHERE good_type_id = 1

+----+-----------------+
| id | name            |
+----+-----------------+
|  1 | Tomatoes        |
|  2 | Carrots         |
|  3 | Brussel Sprouts |
+----+-----------------+

И вот SQL, чтобы получить данные для отображения в таблице:

SELECT qwhen, good_id, sum(quantity) AS total
FROM deliveries d
LEFT JOIN goods g ON d.good_id = g.id
WHERE good_type_id = 1
GROUP BY qwhen, good_id

+------------+---------+-------+
| qwhen      | good_id | total |
+------------+---------+-------+
| 2013-10-01 |       1 |    13 |
| 2013-10-01 |       2 |    20 |
| 2013-10-01 |       3 |    21 |
| 2013-10-02 |       2 |    18 |
| 2013-10-03 |       1 |     4 |
| 2013-10-03 |       2 |     9 |
+------------+---------+-------+

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

// Get the column headers
$sql = 'SELECT id, name FROM goods WHERE good_type_id = 1';
$stmt = $pdo->prepare($sql);
$stmt->execute();

// Start our table.
echo '<table border="1" cellspacing="0"><thead>';

// Print out the headers.
echo '<tr>';
echo '<th>Date</th>';
while ($row = $stmt->fetch()){
    echo '<th>'. $row['name'] .'</th>';
    $columns[$row['id']] = $row['name'];
}
echo '</tr>';
echo '</thead><tbody>';

// Get the data.
$sql = 'SELECT qwhen, good_id, sum(quantity) AS total FROM deliveries d LEFT JOIN goods g ON d.good_id = g.id WHERE good_type_id = 1 GROUP BY qwhen, good_id';
$stmt = $pdo->prepare($sql);
$stmt->execute();

// Manipulate the data into an array.
$save_date = NULL;
while ($row = $stmt->fetch()){
    if ($save_date !== $row['qwhen']){
        $save_date = $row['qwhen'];
        $data[$row['qwhen']] = array();
    }
    $data[$row['qwhen']][$row['good_id']] = $row['total'];
}

// Print out the table data.
foreach ($data AS $date => $cell){
    echo '<tr>';
    echo '<td>'. $date .'</td>';
    foreach ($columns AS $id => $name){
        echo '<td align="right">';
        if (isset($cell[$id])){
            echo $cell[$id];
        }else{
            echo '&nbsp;';
        }
        echo '</td>';
    }
    echo '</tr>';
}

// End our table.
echo '</tbody></table>';

http://i.imgur.com/pX5IIum.jpg

Я придумал альтернативу, используя функцию PostgreSQL crosstab, предложенную @PM77-1.

В частности, я использую crosstab(text source_sql, text category_sql) Форма функции следующая:

SELECT * FROM 
  crosstab('SELECT e.datetime, v.category_id, v.number 
            FROM entries e, values v 
            WHERE v.entry_id = e.id AND e.tracker_id = 1 ORDER BY 1, 2', 
           'SELECT id FROM categories WHERE tracker_id = 1 ORDER BY 1')
  AS (row_name timestamp without time zone,
      tomatoes numeric,
      carrots numeric,
      brussel_sprouts numeric);
При таком подходе термин AS (...) должен быть уникальным для каждого трекера, так как количество категорий и их названия могут отличаться для каждого трекера. В моем случае я выполняю запрос с помощью Python и модуля psycopg2, поэтому его можно легко сгенерировать динамически. Для например,
# Retrieve the category names for the current tracker
cur.execute("SELECT name FROM categories WHERE tracker_id = " + 
            str(tracker_id) + ";")
categories = cur.fetchall()
category_count = len(categories)

# Generate category string
cat_str = '';
for n in range(category_count):
    cat_str = cat_str + ", cat_" + str(n) + " numeric"

cur.execute("SELECT * FROM crosstab("
            "'SELECT e.datetime, v.category_id, v.number FROM entries e, values v"
            " WHERE v.entry_id = e.id"
            " AND e.tracker_id = " + str(tracker_id) +
            " ORDER BY 1, 2;',"
            " 'SELECT id FROM categories WHERE tracker_id =" + 
            str(tracker_id) + "')"
            " AS (row_name timestamp without time zone" + cat_str + ");")

results = cur.fetchall()

Результат имеет общие имена столбцов cat_0, cat_1, etc. вместо tomatoes, carrots, etc.. Однако я передаю и categories, и results в HTML-шаблон, чтобы отобразить таблицу с правильными заголовками.