SQL : Create a full record from 2 tables

末鹿安然 提交于 2019-12-02 10:00:30

问题


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!


回答1:


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:

  • PostgreSQL Crosstab Query

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):

  • How much disk-space is needed to store a NULL value using postgresql DB?
  • Do nullable columns occupy additional space in PostgreSQL?

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:

  • Use case for hstore against multiple columns

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

  • Is there a name for this database structure?



回答2:


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



回答3:


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


来源:https://stackoverflow.com/questions/25186533/sql-create-a-full-record-from-2-tables

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!