'SQL to convert multiple rows into a single row of variable length
I am writing a module for drupal6/PHP5 using MySQL (and optionally PostgreSQL).
To represent arbitrary sample properties and instrument readings in an experiment, I have two tables: one of fields which names the properties and one of parameters which holds the property value for each reading. A parameter will only have a numeric value (FLOAT/REAL) or a string value (TEXT), but not both (according to the field's type
, which is in the Fields
table).
CREATE TABLE Fields (
`fid` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT 'field ID',
`name` VARCHAR(128) NOT NULL,
`type` ENUM('float', 'real', 'text') NOT NULL
-- ...
);
CREATE TABLE Params (
`pid` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT 'param ID',
`lid` INT UNSIGNED NOT NULL,
`fid` INT UNSIGNED NOT NULL COMMENT 'field ID',
`numVal` FLOAT,
`stringVal` VARCHAR(256),
FOREIGN KEY (`fid`) REFERENCES Fields (`fid`)
);
For a given lid
there are multiple/variable numbers of parameters/rows which I can easily retrieve as rows in a loop. How can I retrieve a single row of values, where each column has the associated field name as its heading, for each lid
? In Drupal such a query would return all field:value pairs as object attribute:value pairs in a single database call, which I figure would be more efficient.
By way of example, take the following sample data:
INSERT INTO Fields (`fid`, `name`, `type`)
VALUES
(1, 'voltage', 'float'),
(2, 'current', 'float'),
(3, 'size', 'float'),
(4, 'colour', 'text')
;
INSERT INTO Params (pid, lid, fid, numVal, stringVal)
VALUES
(1, 1, 1, 2.5, NULL),
(2, 1, 4, NULL, 'blue'),
(3, 2, 1, 2.7, NULL),
(4, 2, 2, 13, NULL),
(5, 2, 3, 27, NULL),
(6, 2, 4, NULL, 'greenish blue')
;
For LID=1, the result should be a single row:
voltage | colour |
---|---|
2.5 | 'blue' |
For LID=2:
voltage | current | size | colour |
---|---|---|---|
2.7 | 13 | 27 | 'greenish blue' |
Google has found me a few SQL examples "combining multiple rows into a single row", but they seem to assume pre-ordained labelling of a fixed number of columns/fields in the result set, and/or build column entries as single composite "name:value" strings. I'm hoping there might be a more general approach for arbitrary numbers of fields, that works for both MySQL and PostgreSQL.
I only have very basic SQL experience, so I don't know whats possible regarding views or temporary tables or defining and executing procedures on the fly.
Solution 1:[1]
This displays the data vertically rather than horizontally.
SELECT
FIELDS.NAME AS PROPERTYNAME,
CASE WHEN PARAMS.NUMVAL IS NULL THEN STRINGVAL ELSE CAST(PARAMS.NUMVAL AS VARCHAR(100)) END AS PROPERTYVALUE
FROM
PARAMS, FIELDS
WHERE
PARAMS.FID = FIELDS.FID
AND
PARAMS.LID = 1
ORDER BY
PARAMS.FID
The SQL may need some tweaking, but it is the best I can do without access to your DB schema.
Solution 2:[2]
Pivot table stuff seems just too messy. I am thinking to just go with something simple like this (Thanks John Pick for showing me how to use CASE!):
SELECT p.l_id,
GROUP_CONCAT(
CONCAT(f.name,'=',
CASE WHEN p.stringval IS NULL
THEN p.numval
ELSE p.stringval
END
) SEPARATOR '||'
) AS vals
FROM
params AS p
LEFT JOIN fields AS f
ON p.f_id=f.f_id
WHERE
p.l_id=1
GROUP BY
p.l_id;
and just post-process the "vals" string with explode() in PHP.
+------+---------------------------------------------------------- ... -+
| l_id | vals |
+------+---------------------------------------------------------- ... -+
| 1 | kv=50||ma=50||pressure=0||temp_ccd=-58||temp_sample=150|| ... |
+------+--------------------------------------------------------- ... +
What a cop out! I would've expected a much more elegant solution given that all the information required to return columns in correct format exists in the PARAMS AND FIELDS tables. And now I have to worry about stringvals with SEPARATORs in them :-/
Solution 3:[3]
The difficulty in some RDBMSs with pivoting a variable number of columns shows why EAV is problematic in a relational database. It's an attempt to shoehorn a different object model into the relational model. In these cases, it can be simpler to perform some of the data operations on the programming side (here, PHP). For the DB side, do as much as makes sense with the relational model:
SELECT p.lid,
f.name,
CASE f.type
WHEN 'text' THEN p.stringVal
ELSE p.numVal
END AS value
FROM Fields AS f
JOIN Params AS p ON f.fid = p.fid
WHERE p.lid = ?
Then, take these results and combine them (in PHP):
$stmtReadings = <<<'EOS'
SELECT p.lid, f.name,
-- …
EOS;
$sample = [];
$qryReadings = $db->prepare($stmtReadings);
foreach ($qryReadings->execute([$lid]) as $reading) {
$sample[$reading['name']] = $reading['value'];
}
For multiple samples, the prepared statement could be executed multiple times (once for each sample). For all the samples, it's likely more performant to fetch all params in a single query, and combine them in a single loop.
$stmtReadings = <<<'EOS'
SELECT p.lid,
f.name,
CASE f.type
WHEN 'text' THEN p.stringVal
ELSE p.numVal
END AS value
FROM Fields AS f
JOIN Params AS p ON f.fid = p.fid
ORDER BY p.lid
EOS;
$samples = [];
foreach ($db->query($stmtReadings) as list($lid, $name, $value)) {
$samples[$lid][$name] = $value;
}
Note the above requires the query columns have a specific order. As of PHP 7.1, destructuring assignment can be used for a more robust program:
// …
foreach ($db->query($stmtReadings) as ['lid' => $lid, 'name' => $name, 'value' => $value]) {
$samples[$lid][$name] = $value;
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | John Pick |
Solution 2 | doobs |
Solution 3 | outis |