Табличное представление данных из нормализованной базы данных 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 ответа:
Вот как я бы определил ваши таблицы:
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 ' '; } echo '</td>'; } echo '</tr>'; } // End our table. echo '</tbody></table>';
Я придумал альтернативу, используя функцию 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-шаблон, чтобы отобразить таблицу с правильными заголовками.