1

I've got a DB structure as is (simplified to maximum for understanding concern):

Table "entry" ("id" integer primary key)
Table "fields" ("name" varchar primary key, and others)
Table "entry_fields" ("entryid" integer primary key, "name" varchar primary key, "value")

I would like to get, for a given "entry.id", the detail of this entry, ie. all the "entry_fields" linked to this entry, in a single SQL query.

An example would be better perhaps:

"fields":

"result"
"output"
"code"
"command"

"entry" contains:

id : 842
id : 850

"entry_fields" contains:

entryid : 842, name : "result", value : "ok"
entryid : 842, name : "output", value : "this is an output"
entryid : 842, name : "code", value : "42"
entryid : 850, name : "result", value : "ko"
entryid : 850, name : "command", value : "print ko"

The wanted output would be:

| id  | command    | output               | code | result |
| 842 | NULL       | "this is an output"  | 42   | ok     |
| 850 | "print ko" | NULL                 | NULL | ko     |

The aim is to be able to add a "field" without changing anything to "entry" table structure

I tried something like:

SELECT e.*, (SELECT name FROM fields) FROM entry AS e

but Postgres complains:

ERROR: more than one row returned by a subquery used as an expression

Hope someone can help me!

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
ChPortos
  • 75
  • 1
  • 4

3 Answers3

2

Solution as requested

While stuck with this unfortunate design, the fastest query would be with crosstab(), provided by the additional module tablefunc. Ample details in this related answer:

For the question asked:

SELECT * FROM crosstab(
      $$SELECT e.id, ef.name, ef.value
       FROM   entry             e
       LEFT   JOIN entry_fields ef
              ON ef.entryid = e.id
             AND ef.name = ANY ('{result,output,code,command}'::text[])
       ORDER  BY 1, 2$$

     ,$$SELECT unnest('{result,output,code,command}'::text[])$$
   ) AS ct (id int, result text, output text, code text, command text);

Database design

If you don't have a huge number of different fields, it will be much simpler and more efficient to merge all three tables into one simple table:

CREATE TABLE entry (
   entry_id serial PRIMARY KEY
  ,field1   text
  ,field2   text
  , ... more fields
);

Fields without values can be NULL. NULL storage is very cheap (basically 1 bit per column in the NULL bitmap):

Even if you have hundreds of different columns, and only few are filled per entry, this will still use much less disk space.

You query becomes trivial:

SELECT entry_id, result, output, code, command
FROM   enty;

If you have too many columns1, and that's not just a misguided design (often, this can be folded into much fewer columns), consider the data types hstore or json / jsonb (in Postgres 9.4) for EAV storage.

1 Per Postgres "About" page:

Maximum Columns per Table   250 - 1600 depending on column types

Consider this related answer with alternatives:

And this question about typical use cases / problems of EAV structures on dba.SE:

Community
  • 1
  • 1
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • I though about this simpler design first, but then I loose the ability to be able to add fields without changing the table structure, and have to grant the "ALTER TABLE" right to a simple user ? – ChPortos Aug 08 '14 at 06:42
  • ... but yeah, after reading your link about EAV, I'll rethink about the correct DB design to use. Thanks !!! – ChPortos Aug 08 '14 at 07:59
1

Dynamic SQL:

CREATE TABLE fields (name varchar(100) PRIMARY KEY)
INSERT INTO FIELDS VALUES ('RESULT')
INSERT INTO FIELDS VALUES ('OUTPUT')
INSERT INTO FIELDS VALUES ('CODE')
INSERT INTO FIELDS VALUES ('COMMAND')

CREATE TABLE ENTRY_fields (ENTRYID INT, name varchar(100), VALUE VARCHAR(100) CONSTRAINT PK PRIMARY KEY(ENTRYID, name))
INSERT INTO ENTRY_fields VALUES(842, 'RESULT', 'OK')
INSERT INTO ENTRY_fields VALUES(842, 'OUTPUT', 'THIS IS AN OUTPUT')
INSERT INTO ENTRY_fields VALUES(842, 'CODE', '42')
INSERT INTO ENTRY_fields VALUES(850, 'RESULT', 'KO')
INSERT INTO ENTRY_fields VALUES(850, 'COMMAND', 'PRINT KO')

CREATE TABLE ENTRY (ID INT PRIMARY KEY)
INSERT INTO ENTRY VALUES(842)
INSERT INTO ENTRY VALUES(850)

DECLARE @COLS NVARCHAR(MAX), @SQL NVARCHAR(MAX)

select @Cols = stuff((select ', ' + quotename(dt)
from (select DISTINCT name as dt 
from fields) X
FOR XML PATH('')),1,2,'')

PRINT @COLS
SET @SQL = 'SELECT * FROM (SELECT id, f.name, value
from fields F CROSS join ENTRY LEFT JOIN entry_fields ef on ef.name =  f.name AND ID = ef.ENTRYID
) Y PIVOT (max(value) for name in ('+ @Cols +'))PVT '

--print @SQL

exec (@SQL)

If you think your values are going to be constant in the fields table:

SELECT * FROM (SELECT id, f.name ,value
from fields F CROSS join ENTRY LEFT JOIN entry_fields ef on ef.name =  f.name AND ID = ef.ENTRYID
) Y PIVOT (max(value) for name in ([CODE], [COMMAND], [OUTPUT], [RESULT]))PVT

Query that may work with postgresql:

    SELECT ID, MAX(CODE) as CODE, MAX(COMMAND) as COMMAND, MAX(OUTPUT) as OUTPUT, MAX(RESULT) as RESULT
FROM (SELECT ID, 
      CASE WHEN f.name = 'CODE' THEN VALUE END AS CODE, 
      CASE WHEN f.name = 'COMMAND' THEN VALUE END AS COMMAND,
      CASE WHEN f.name = 'OUTPUT' THEN VALUE END AS OUTPUT,
      CASE WHEN f.name = 'RESULT' THEN VALUE END AS RESULT
from fields F CROSS join ENTRY LEFT JOIN entry_fields ef on ef.name =  f.name AND ID = ENTRYID
) Y
GROUP BY ID
Djinni
  • 19
  • 2
0

The subquery (SELECT name FROM fields) would return 4 rows. You can't stuff 4 rows into 1 in SQL. You can use crosstab, which I'm not familiar enough to answer. Or you can use a crude query like this:

SELECT   e.*,
         (SELECT value FROM entry_fields AS ef WHERE name = 'command' AND ef.entryid = f.entryid) AS command,
         (SELECT value FROM entry_fields AS ef WHERE name = 'output' AND ef.entryid = f.entryid) AS output,
         (SELECT value FROM entry_fields AS ef WHERE name = 'code' AND ef.entryid = f.entryid) AS code,
         (SELECT value FROM entry_fields AS ef WHERE name = 'result' AND ef.entryid = f.entryid) AS result
FROM     entry AS e
Code Different
  • 90,614
  • 16
  • 144
  • 163