PostgreSQL DISTINCT ON dengan ORDER BY yang berbeda

216

Saya ingin menjalankan kueri ini:

SELECT DISTINCT ON (address_id) purchases.address_id, purchases.*
FROM purchases
WHERE purchases.product_id = 1
ORDER BY purchases.purchased_at DESC

Tapi saya mendapatkan kesalahan ini:

PG :: Kesalahan: ERROR: SELECT DISTINCT ON ekspresi harus cocok dengan ekspresi ORDER BY awal

Menambahkan address_idsebagai ORDER BYekspresi pertama membungkam kesalahan, tetapi saya benar-benar tidak ingin menambahkan penyortiran address_id. Apakah mungkin dilakukan tanpa memesan address_id?

sl_bug
sumber
Klausa pesanan Anda telah dibeli_tidak alamat_id.Bisakah Anda menjelaskan pertanyaan Anda.
Teja
pesanan saya telah dibeli karena saya menginginkannya, tetapi postgres juga meminta alamat (lihat pesan kesalahan).
sl_bug
3
Sepenuhnya dijawab di sini - stackoverflow.com/questions/9796078/... Terima kasih kepada stackoverflow.com/users/268273/mosty-mostacho
sl_bug
Secara pribadi saya pikir memerlukan DISTINCT ON untuk mencocokkan ORDER BY sangat dipertanyakan, karena ada berbagai kasus penggunaan yang sah untuk membuat mereka berbeda. Ada posting di postgresql.uservoice yang mencoba mengubah ini untuk mereka yang merasakan hal yang sama. postgresql.uservoice.com/forums/21853-general/suggestions/…
titik koma
mendapat masalah yang sama persis, dan menghadapi batasan yang sama. Saat ini saya telah memecahnya menjadi sub-permintaan dan kemudian memesan, tetapi rasanya kotor.
Taman Guy

Jawaban:

207

Dokumentasi mengatakan:

DISTINCT ON (ekspresi [, ...]) hanya menyimpan baris pertama dari setiap rangkaian di mana ekspresi yang diberikan bernilai sama. [...] Perhatikan bahwa "baris pertama" dari setiap set tidak dapat diprediksi kecuali ORDER BY digunakan untuk memastikan bahwa baris yang diinginkan muncul terlebih dahulu. [...] Ekspresi DISTINCT ON harus cocok dengan ekspresi ORDER BY paling kiri.

Dokumentasi resmi

Jadi, Anda harus menambahkan address_idke pesanan dengan.

Atau, jika Anda mencari baris penuh yang berisi produk yang paling baru dibeli untuk masing-masing address_iddan hasil yang diurutkan purchased_atkemudian Anda mencoba untuk memecahkan masalah N per kelompok terbesar yang dapat diselesaikan dengan pendekatan berikut:

Solusi umum yang harus bekerja di sebagian besar DBMS:

SELECT t1.* FROM purchases t1
JOIN (
    SELECT address_id, max(purchased_at) max_purchased_at
    FROM purchases
    WHERE product_id = 1
    GROUP BY address_id
) t2
ON t1.address_id = t2.address_id AND t1.purchased_at = t2.max_purchased_at
ORDER BY t1.purchased_at DESC

Solusi berorientasi PostgreSQL yang lebih berdasarkan pada jawaban @ hkf:

SELECT * FROM (
  SELECT DISTINCT ON (address_id) *
  FROM purchases 
  WHERE product_id = 1
  ORDER BY address_id, purchased_at DESC
) t
ORDER BY purchased_at DESC

Masalah diklarifikasi, diperluas dan diselesaikan di sini: Memilih baris yang dipesan oleh beberapa kolom dan berbeda pada yang lain

Mosty Mostacho
sumber
40
Ini bekerja, tetapi memberikan urutan yang salah. Itu sebabnya saya ingin menyingkirkan address_id dalam rangka klausa
sl_bug
1
Dokumentasi jelas: Anda tidak dapat karena baris yang dipilih tidak dapat diprediksi
Mosty Mostacho
3
Tetapi mungkin ada cara lain untuk memilih pembelian terbaru untuk alamat yang jauh?
sl_bug
1
Jika Anda perlu order by purchases.purchased_at, Anda dapat menambahkan purchased_at kondisi DISTINCT Anda: SELECT DISTINCT ON (purchases.purchased_at, address_id). Namun, dua catatan dengan address_id yang sama tetapi nilai buying_at yang berbeda akan menghasilkan duplikat di set yang dikembalikan. Pastikan Anda mengetahui data yang Anda tanyakan.
Brendan Benson
23
Semangat pertanyaannya jelas. Tidak perlu memilih semantik. Sangat menyedihkan bahwa jawaban yang diterima dan paling banyak dipilih tidak membantu Anda memecahkan masalah.
nicooga
55

Anda dapat memesan dengan address_id dalam subquery, lalu memesan dengan apa yang Anda inginkan dalam kueri luar.

SELECT * FROM 
    (SELECT DISTINCT ON (address_id) purchases.address_id, purchases.* 
    FROM "purchases" 
    WHERE "purchases"."product_id" = 1 ORDER BY address_id DESC ) 
ORDER BY purchased_at DESC
hkf
sumber
3
Tapi ini akan lebih lambat dari hanya satu permintaan, bukan?
sl_bug
2
Sangat sedikit ya. Meskipun sejak Anda melakukan pembelian. * Dalam dokumen asli Anda select, saya rasa ini bukan kode produksi?
hkf
8
Saya akan menambahkan bahwa untuk versi postgres yang lebih baru Anda perlu alias subquery. Sebagai contoh: SELECT * FROM (SELECT DISTINCT ON ON (address_id) purchase.address_id, pembelian. * DARI "pembelian" DI MANA "pembelian". "Product_id" = 1 ORDER DENGAN address_id DESC) SEBAGAI PEMESANAN DENGAN tmp.purchased_at DESC
aembke
Ini akan kembali address_iddua kali (tanpa perlu). Banyak klien memiliki masalah dengan nama kolom duplikat. ORDER BY address_id DESCtidak ada gunanya dan menyesatkan. Itu tidak berguna dalam permintaan ini. Hasilnya adalah pemilihan acak dari setiap rangkaian baris dengan yang sama address_id, bukan baris dengan yang terbaru purchased_at. Pertanyaan ambigu tidak menanyakan hal itu secara eksplisit, tetapi itu hampir pasti maksud OP. Singkatnya: jangan gunakan kueri ini . Saya memposting alternatif dengan penjelasan.
Erwin Brandstetter
Bekerja untukku. Jawaban yang bagus
Matt West
46

Sebuah subquery bisa mengatasinya:

SELECT *
FROM  (
    SELECT DISTINCT ON (address_id) *
    FROM   purchases
    WHERE  product_id = 1
    ) p
ORDER  BY purchased_at DESC;

Ekspresi terkemuka di ORDER BYharus setuju dengan kolom di DISTINCT ON, sehingga Anda tidak dapat memesan dengan kolom yang berbeda di kolom yang sama SELECT.

Hanya gunakan tambahan ORDER BYdi subquery jika Anda ingin memilih baris tertentu dari setiap set:

SELECT *
FROM  (
    SELECT DISTINCT ON (address_id) *
    FROM   purchases
    WHERE  product_id = 1
    ORDER  BY address_id, purchased_at DESC  -- get "latest" row per address_id
    ) p
ORDER  BY purchased_at DESC;

Jika purchased_atbisa NULL, pertimbangkan DESC NULLS LAST. Tetapi pastikan untuk mencocokkan indeks Anda jika Anda ingin menggunakannya. Lihat:

Terkait, dengan penjelasan lebih lanjut:

Erwin Brandstetter
sumber
Anda tidak dapat menggunakan DISTINCT ONtanpa pencocokan ORDER BY. Permintaan pertama membutuhkan bagian ORDER BY address_iddalam subquery.
Aristoteles Pagaltzis
4
@AristotlePagaltzis: Tapi Anda bisa . Dari mana pun Anda mendapatkannya, itu tidak benar. Anda dapat menggunakan DISTINCT ONtanpa ORDER BYdi kueri yang sama. Anda mendapatkan baris sewenang-wenang dari setiap set rekan yang ditentukan oleh DISTINCT ONklausa dalam kasus ini. Cobalah atau ikuti tautan di atas untuk detail dan tautan ke manual. ORDER BYdalam permintaan yang sama (sama SELECT) tidak bisa tidak setuju dengan DISTINCT ON. Saya memang menjelaskan itu juga.
Erwin Brandstetter
Hah, kamu benar. Saya buta terhadap implikasi dari catatan "tidak dapat diprediksi kecuali ORDER BYdigunakan" dalam dokumen karena tidak masuk akal bagi saya bahwa fitur tersebut diterapkan untuk dapat menangani set nilai yang tidak berurutan ... namun tidak akan memungkinkan Anda untuk mengeksploitasinya dengan pemesanan eksplisit. Mengganggu.
Aristoteles Pagaltzis
@AristotlePagaltzis: Itu karena, secara internal, Postgres menggunakan salah satu dari (setidaknya) dua algoritma yang berbeda: baik melintasi daftar yang diurutkan atau bekerja dengan nilai hash - mana yang menjanjikan untuk lebih cepat. Dalam kasus selanjutnya hasilnya tidak diurutkan berdasarkan DISTINCT ONekspresi (belum).
Erwin Brandstetter
2
Terima kasih. Jawaban Anda selalu jelas dan bermanfaat!
Andrey Deineko
10

Fungsi jendela dapat menyelesaikannya dalam satu pass:

SELECT DISTINCT ON (address_id) 
   LAST_VALUE(purchases.address_id) OVER wnd AS address_id
FROM "purchases"
WHERE "purchases"."product_id" = 1
WINDOW wnd AS (
   PARTITION BY address_id ORDER BY purchases.purchased_at DESC
   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
savenkov
sumber
7
Akan lebih baik jika seseorang menjelaskan kueri.
Gajus
@Gajus: Penjelasan singkat: tidak berfungsi, hanya menghasilkan berbeda address_id. Prinsipnya bisa bekerja. Contoh terkait: stackoverflow.com/a/22064571/939860 atau stackoverflow.com/a/11533808/939860 . Tetapi ada pertanyaan yang lebih pendek dan / atau lebih cepat untuk masalah yang dihadapi.
Erwin Brandstetter
5

Bagi siapa pun yang menggunakan Flask-SQLAlchemy, ini bekerja untuk saya

from app import db
from app.models import Purchases
from sqlalchemy.orm import aliased
from sqlalchemy import desc

stmt = Purchases.query.distinct(Purchases.address_id).subquery('purchases')
alias = aliased(Purchases, stmt)
distinct = db.session.query(alias)
distinct.order_by(desc(alias.purchased_at))
Ruben
sumber
2
Ya, atau bahkan lebih mudah, saya dapat menggunakan:query.distinct(foo).from_self().order(bar)
Laurent Meyer
@LaurentMeyer maksudmu Purchases.query?
Reubano
Ya, maksud saya Purchases.query
Laurent Meyer
-2

Anda juga dapat melakukan ini dengan menggunakan grup dengan klausa

   SELECT purchases.address_id, purchases.* FROM "purchases"
    WHERE "purchases"."product_id" = 1 GROUP BY address_id,
purchases.purchased_at ORDER purchases.purchased_at DESC
Vaishali
sumber
Ini tidak benar (kecuali purchaseshanya memiliki dua kolom address_iddan purchased_at). Karena itu GROUP BY, Anda harus menggunakan fungsi agregat untuk mendapatkan nilai dari setiap kolom yang tidak digunakan untuk pengelompokan, sehingga nilai-nilai mereka semua akan berasal dari baris yang berbeda dari grup kecuali Anda pergi melalui senam yang jelek dan tidak efisien. Ini dapat diperbaiki hanya dengan menggunakan fungsi jendela daripada GROUP BY.
Aristoteles Pagaltzis