Bagaimana cara membuat CROSS JOIN yang diputar di mana definisi tabel yang dihasilkan tidak diketahui?

17

Diberikan dua tabel dengan jumlah baris yang tidak ditentukan dengan nama dan nilai, bagaimana saya akan menampilkan pivot CROSS JOINdari fungsi di atas nilainya.

CREATE TEMP TABLE foo AS
SELECT x::text AS name, x::int
FROM generate_series(1,10) AS t(x);

CREATE TEMP TABLE bar AS
SELECT x::text AS name, x::int
FROM generate_series(1,5) AS t(x);

Misalnya, jika fungsi itu adalah perkalian, bagaimana saya menghasilkan tabel (perkalian) seperti di bawah ini,

Tabel perkalian umum dari 1..12

Semua (arg1,arg2,result)baris tersebut dapat dibuat dengan

SELECT foo.name AS arg1, bar.name AS arg2, foo.x*bar.x AS result
FROM foo
CROSS JOIN bar; 

Jadi ini hanya masalah presentasi, saya ingin ini juga bekerja dengan nama kustom - nama yang tidak hanya argumen CASTed ke teks tetapi diatur dalam tabel,

CREATE TEMP TABLE foo AS
SELECT chr(x+64) AS name, x::int
FROM generate_series(1,10) AS t(x);

CREATE TEMP TABLE bar AS
SELECT chr(x+72) AS name, x::int
FROM generate_series(1,5) AS t(x);

Saya pikir ini akan mudah dilakukan dengan CROSSTAB yang mampu tipe kembali yang dinamis.

SELECT * FROM crosstab(
  '
    SELECT foo.x AS arg1, bar.x AS arg2, foo.x*bar.x
    FROM foo
    CROSS JOIN bar
  ', 'SELECT DISTINCT name FROM bar'
) AS **MAGIC**

Tapi, tanpa itu **MAGIC**, aku mengerti

ERROR:  a column definition list is required for functions returning "record"
LINE 1: SELECT * FROM crosstab(

Untuk referensi, menggunakan contoh di atas dengan nama ini adalah sesuatu yang lebih seperti apa tablefunc's crosstab()keinginan.

SELECT * FROM crosstab(
  '
    SELECT foo.x AS arg1, bar.x AS arg2, foo.x*bar.x
    FROM foo
    CROSS JOIN bar
  '
) AS t(row int, i int, j int, k int, l int, m int);

Tapi, sekarang kita kembali membuat asumsi tentang konten dan ukuran bartabel dalam contoh kita. Jadi jika,

  1. Tabel panjangnya tidak ditentukan,
  2. Kemudian cross-join mewakili kubus dimensi yang tidak ditentukan (karena di atas),
  3. Nama-nama katagori (istilah tabulasi silang) ada di tabel

Apa yang terbaik yang dapat kita lakukan di PostgreSQL tanpa "daftar definisi kolom" untuk menghasilkan presentasi seperti itu?

Evan Carroll
sumber
1
Apakah hasil JSON akan menjadi pendekatan yang baik? Apakah ARRAY akan menjadi pendekatan yang bagus? Dengan cara ini, definisi "tabel output" sudah diketahui (dan diperbaiki). Anda menempatkan fleksibilitas dalam JSON atau ARRAY. Saya kira itu akan tergantung banyak alat yang digunakan setelahnya untuk memproses informasi.
joanolo
Saya lebih suka seperti di atas, jika memungkinkan.
Evan Carroll

Jawaban:

11

Kasus sederhana, SQL statis

Solusi non-dinamis dengan crosstab()untuk kasus sederhana:

SELECT * FROM crosstab(
  'SELECT b.x, f.name, f.x * b.x AS prod
   FROM   foo f, bar b
   ORDER  BY 1, 2'
   ) AS ct (x int, "A" int, "B" int, "C" int, "D" int, "E" int
                 , "F" int, "G" int, "H" int, "I" int, "J" int);

Saya memesan kolom yang dihasilkan dengan foo.name, bukan foo.x. Keduanya kebetulan dipilah secara paralel, tapi itu hanya pengaturan sederhana. Pilih urutan pengurutan yang tepat untuk kasus Anda. Sebenarnya nilai dari kolom kedua adalah tidak relevan dalam query ini (bentuk 1-parameter crosstab()).

Kami bahkan tidak perlu crosstab()dengan 2 parameter karena tidak ada nilai yang hilang menurut definisi. Lihat:

(Anda tetap query crosstab dalam pertanyaan dengan mengganti foodengan bardalam mengedit kemudian. Ini juga perbaikan query, tetapi terus bekerja dengan nama-nama dari foo.)

Jenis pengembalian tidak dikenal, SQL dinamis

Nama dan tipe kolom tidak boleh dinamis. SQL menuntut untuk mengetahui jumlah, nama, dan jenis kolom yang dihasilkan pada waktu panggilan. Entah dengan deklarasi eksplisit atau dari informasi dalam katalog sistem (Itulah yang terjadi dengan SELECT * FROM tbl: Postgres mencari definisi tabel yang terdaftar.)

Anda ingin Postgres mendapatkan kolom yang dihasilkan dari data di tabel pengguna. Tidak akan terjadi.

Dengan satu atau lain cara, Anda perlu dua perjalanan pulang pergi ke server. Entah Anda membuat kursor dan kemudian berjalan melewatinya. Atau Anda membuat tabel temp dan kemudian memilihnya. Atau Anda mendaftarkan jenis dan menggunakannya dalam panggilan.

Atau Anda cukup membuat kueri dalam satu langkah dan menjalankannya di langkah berikutnya:

SELECT $$SELECT * FROM crosstab(
  'SELECT b.x, f.name, f.x * b.x AS prod
   FROM   foo f, bar b
   ORDER  BY 1, 2'
   ) AS ct (x int, $$
 || string_agg(quote_ident(name), ' int, ' ORDER BY name) || ' int)'
FROM   foo;

Ini menghasilkan kueri di atas, secara dinamis. Jalankan di langkah berikutnya.

Saya menggunakan dolar-tanda kutip ( $$) untuk menjaga penanganan kutipan bersarang sederhana. Lihat:

quote_ident() sangat penting untuk menghindari nama kolom yang tidak sah (dan mungkin bertahan terhadap injeksi SQL).

Terkait:

Erwin Brandstetter
sumber
Saya perhatikan bahwa menjalankan kueri yang Anda sebut "Jenis pengembalian tidak dikenal, SQL dinamis" sebenarnya hanya mengembalikan string yang mewakili permintaan lain, dan kemudian Anda mengatakan "jalankan di langkah berikutnya". Apakah ini berarti bahwa akan sulit misalnya untuk membuat pandangan terwujud dari ini?
Colin D
@ColinD: Tidak sulit, tapi jelas tidak mungkin. Anda dapat membuat MV dari SQL yang dihasilkan dengan tipe pengembalian yang dikenal. Tetapi Anda tidak dapat memiliki MV dengan tipe pengembalian yang tidak diketahui.
Erwin Brandstetter
10

Apa yang terbaik yang dapat kita lakukan di PostgreSQL tanpa "daftar definisi kolom" untuk menghasilkan presentasi seperti itu?

Jika Anda membingkai ini sebagai masalah presentasi, Anda mungkin mempertimbangkan fitur presentasi post-query.

Versi yang lebih baru dari psql(9.6) datang dengan \crosstabview, menunjukkan hasil dalam representasi tab silang tanpa dukungan SQL (karena SQL tidak dapat menghasilkan ini secara langsung, seperti yang disebutkan dalam jawaban @ Erwin: SQL menuntut untuk mengetahui jumlah, nama dan jenis kolom yang dihasilkan pada waktu panggilan )

Misalnya, permintaan pertama Anda memberi:

SELECT foo.name AS arg1, bar.name AS arg2, foo.x*bar.x AS result
FROM foo
CROSS JOIN bar
\crosstabview

 arg1 | 1  | 2  | 3  | 4  | 5  
------+----+----+----+----+----
 1    |  1 |  2 |  3 |  4 |  5
 2    |  2 |  4 |  6 |  8 | 10
 3    |  3 |  6 |  9 | 12 | 15
 4    |  4 |  8 | 12 | 16 | 20
 5    |  5 | 10 | 15 | 20 | 25
 6    |  6 | 12 | 18 | 24 | 30
 7    |  7 | 14 | 21 | 28 | 35
 8    |  8 | 16 | 24 | 32 | 40
 9    |  9 | 18 | 27 | 36 | 45
 10   | 10 | 20 | 30 | 40 | 50
(10 rows)

Contoh kedua dengan nama kolom ASCII memberi:

SELECT foo.name AS arg1, bar.name AS arg2, foo.x*bar.x
    FROM foo
    CROSS JOIN bar
  \crosstabview

 arg1 | I  | J  | K  | L  | M  
------+----+----+----+----+----
 A    |  1 |  2 |  3 |  4 |  5
 B    |  2 |  4 |  6 |  8 | 10
 C    |  3 |  6 |  9 | 12 | 15
 D    |  4 |  8 | 12 | 16 | 20
 E    |  5 | 10 | 15 | 20 | 25
 F    |  6 | 12 | 18 | 24 | 30
 G    |  7 | 14 | 21 | 28 | 35
 H    |  8 | 16 | 24 | 32 | 40
 I    |  9 | 18 | 27 | 36 | 45
 J    | 10 | 20 | 30 | 40 | 50
(10 rows)

Lihat manual psql dan https://wiki.postgresql.org/wiki/Crosstabview untuk informasi lebih lanjut.

Daniel Vérité
sumber
1
Ini sangat keren.
Evan Carroll
1
Penanganan yang paling elegan.
Erwin Brandstetter
1

Ini bukan solusi yang pasti

Ini adalah pendekatan terbaik saya sampai sekarang. Masih perlu mengubah array terakhir menjadi kolom.

Pertama saya mendapatkan produk Cartesian dari kedua tabel:

select foo.name xname, bar.name yname, (foo.x * bar.x)::text as val,
       ((row_number() over ()) - 1) / (select count(*)::integer from foo) as row
 from bar
     cross join foo
 order by bar.name, foo.name

Tapi, saya telah menambahkan nomor baris hanya untuk mengidentifikasi setiap baris dari tabel pertama.

((row_number() over ()) - 1) / (select count(*)::integer from foo)

Maka saya telah menambahkan hasilnya dalam format ini:

[Row name] [Array of values]


select col_name, values
from
(
select '' as col_name, array_agg(name) as values from foo
UNION
select fy.name as col_name,
    (select array_agg(t.val) as values
    from  
        (select foo.name xname, bar.name yname, (foo.x * bar.x)::text as val,
              ((row_number() over ()) - 1) / (select count(*)::integer from foo) as row
        from bar
           cross join foo
        order by bar.name, foo.name) t
    where t.row = fy.row)
from
    (select name, (row_number() over(order by name)) - 1 as row from bar) fy
) a
order by col_name;

+---+---------------------+
|   |      ABCDEFGHIJ     |
+---+---------------------+
| I |     12345678910     |
+---+---------------------+
| J |   2468101214161820  |
+---+---------------------+
| K |  36912151821242730  |
+---+---------------------+
| L |  481216202428323640 |
+---+---------------------+
| M | 5101520253035404550 |
+---+---------------------+ 

Mengubahnya menjadi string yang dibatasi oleh koma:

select col_name, values
from
(
select '' as col_name, array_to_string(array_agg(name),',') as values from foo
UNION
select fy.name as col_name,
    (select array_to_string(array_agg(t.val),',') as values
    from  
        (select foo.name xname, bar.name yname, (foo.x * bar.x)::text as val,
              ((row_number() over ()) - 1) / (select count(*)::integer from foo) as row
        from bar
           cross join foo
        order by bar.name, foo.name) t
    where t.row = fy.row)
from
    (select name, (row_number() over(order by name)) - 1 as row from bar) fy
) a
order by col_name;


+---+------------------------------+
|   | A,B,C,D,E,F,G,H,I,J          |
+---+------------------------------+
| I | 1,2,3,4,5,6,7,8,9,10         |
+---+------------------------------+
| J | 2,4,6,8,10,12,14,16,18,20    |
+---+------------------------------+
| K | 3,6,9,12,15,18,21,24,27,30   |
+---+------------------------------+
| L | 4,8,12,16,20,24,28,32,36,40  |
+---+------------------------------+
| M | 5,10,15,20,25,30,35,40,45,50 |
+---+------------------------------+

(Hanya untuk mencobanya nanti: http://rextester.com/NBCYXA2183 )

McNets
sumber