Pilih baris pertama di setiap grup GROUP BY?

1326

Seperti judulnya, saya ingin memilih baris pertama dari setiap set baris yang dikelompokkan dengan a GROUP BY.

Khususnya, jika saya punya purchasestabel yang terlihat seperti ini:

SELECT * FROM purchases;

Output saya:

id | pelanggan | total
--- + ---------- + ------
 1 | Joe | 5
 2 | Sally | 3
 3 | Joe | 2
 4 | Sally | 1

Saya ingin menanyakan idtentang pembelian terbesar ( total) yang dilakukan oleh masing-masing customer. Sesuatu seperti ini:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY total DESC;

Output yang Diharapkan:

PERTAMA (id) | pelanggan | PERTAMA (total)
---------- + ---------- + -------------
        1 | Joe | 5
        2 | Sally | 3
David Wolever
sumber
karena Anda hanya mencari masing-masing yang terbesar, mengapa tidak meminta MAX(total)?
phil294
4
@ phil294 kueri untuk max (total) tidak akan mengaitkan total itu dengan nilai 'id' dari baris di mana ia terjadi.
gwideman

Jawaban:

1117

Pada Oracle 9.2+ (bukan 8i + seperti yang dinyatakan sebelumnya), SQL Server 2005+, PostgreSQL 8.4+, DB2, Firebird 3.0+, Teradata, Sybase, Vertica:

WITH summary AS (
    SELECT p.id, 
           p.customer, 
           p.total, 
           ROW_NUMBER() OVER(PARTITION BY p.customer 
                                 ORDER BY p.total DESC) AS rk
      FROM PURCHASES p)
SELECT s.*
  FROM summary s
 WHERE s.rk = 1

Didukung oleh basis data apa pun:

Tetapi Anda perlu menambahkan logika untuk memutuskan hubungan:

  SELECT MIN(x.id),  -- change to MAX if you want the highest
         x.customer, 
         x.total
    FROM PURCHASES x
    JOIN (SELECT p.customer,
                 MAX(total) AS max_total
            FROM PURCHASES p
        GROUP BY p.customer) y ON y.customer = x.customer
                              AND y.max_total = x.total
GROUP BY x.customer, x.total
OMG Ponies
sumber
2
Informix 12.x juga mendukung fungsi-fungsi jendela (CTE perlu dikonversi ke tabel turunan). Dan Firebird 3.0 juga akan mendukung fungsi Window
a_horse_with_no_name
37
ROW_NUMBER() OVER(PARTITION BY [...])bersama dengan beberapa optimasi lainnya membantu saya menurunkan kueri dari 30 detik menjadi beberapa milidetik. Terima kasih! (PostgreSQL 9.2)
Sam
8
Jika ada beberapa pembelian dengan jumlah tertinggi yang sama totaluntuk satu pelanggan, permintaan pertama mengembalikan pemenang yang arbitrer (tergantung pada detail implementasi; iddapat berubah untuk setiap eksekusi!). Biasanya (tidak selalu) Anda ingin satu baris per pelanggan, ditentukan oleh kriteria tambahan seperti "yang dengan yang terkecil id". Untuk memperbaikinya, tambahkan idke ORDER BYdaftar row_number(). Kemudian Anda mendapatkan hasil yang sama dengan kueri ke - 2 , yang sangat tidak efisien untuk kasus ini. Selain itu, Anda memerlukan subquery lain untuk setiap kolom tambahan.
Erwin Brandstetter
2
Google BigQuery juga mendukung perintah ROW_NUMBER () kueri pertama. Bekerja seperti pesona bagi kita
Praxiteles
2
Perhatikan bahwa versi pertama dengan fungsi jendela berfungsi pada SQLite versi 3.25.0: sqlite.org/windowfunctions.html#history
brianz
1150

Dalam PostgreSQL ini biasanya lebih sederhana dan lebih cepat (lebih banyak optimasi kinerja di bawah):

SELECT DISTINCT ON (customer)
       id, customer, total
FROM   purchases
ORDER  BY customer, total DESC, id;

Atau lebih pendek (jika tidak sejelas) dengan nomor urut kolom output:

SELECT DISTINCT ON (2)
       id, customer, total
FROM   purchases
ORDER  BY 2, 3 DESC, 1;

Jika totalbisa NULL (tidak ada salahnya, tetapi Anda ingin mencocokkan indeks yang ada ):

...
ORDER  BY customer, total DESC NULLS LAST, id;

Poin utama

  • DISTINCT ONadalah ekstensi PostgreSQL dari standar (di mana hanya DISTINCTpada seluruh SELECTdaftar didefinisikan).

  • Daftar sejumlah ekspresi dalam DISTINCT ONklausa, nilai baris gabungan mendefinisikan duplikat. Manual:

    Jelas, dua baris dianggap berbeda jika mereka berbeda setidaknya dalam satu nilai kolom. Nilai kosong dianggap sama dalam perbandingan ini.

    Penekanan berani saya.

  • DISTINCT ONdapat dikombinasikan dengan ORDER BY. Ekspresi terkemuka di ORDER BYharus dalam rangkaian ekspresi DISTINCT ON, tetapi Anda dapat mengatur ulang urutan di antara mereka secara bebas. Contoh. Anda dapat menambahkan ekspresi tambahanORDER BY untuk memilih baris tertentu dari setiap grup rekan. Atau, seperti yang dikatakan manual :

    The DISTINCT ONekspresi (s) harus sesuai dengan paling kiri ORDER BY ekspresi (s). The ORDER BYklausul biasanya akan berisi ekspresi tambahan (s) yang menentukan didahulukan diinginkan baris dalam setiap DISTINCT ONkelompok.

    Saya menambahkan idsebagai item terakhir untuk memutuskan hubungan:
    "Pilih baris dengan yang terkecil iddari masing-masing kelompok yang berbagi tertinggi total."

    Untuk memesan hasil dengan cara yang tidak setuju dengan urutan yang menentukan yang pertama per grup, Anda dapat membuat sarang permintaan di atas dalam permintaan luar dengan yang lain ORDER BY. Contoh.

  • Jika totalbisa NULL, Anda kemungkinan besar menginginkan baris dengan nilai bukan nol terbesar. Tambahkan NULLS LASTseperti yang ditunjukkan. Lihat:

  • The SELECTdaftar tidak dibatasi oleh ekspresi dalam DISTINCT ONatau ORDER BYdengan cara apapun. (Tidak diperlukan dalam kasus sederhana di atas):

    • Anda tidak harus memasukkan ekspresi apa pun di DISTINCT ONatau ORDER BY.

    • Anda dapat memasukkan ekspresi lain dalam SELECTdaftar. Ini penting untuk mengganti permintaan yang jauh lebih kompleks dengan fungsi subqueries dan agregat / jendela.

  • Saya diuji dengan Postgres versi 8.3 - 12. Tetapi fitur tersebut sudah ada setidaknya sejak versi 7.1, jadi pada dasarnya selalu.

Indeks

The sempurna indeks untuk query di atas akan menjadi indeks multi-kolom yang mencakup semua tiga kolom dalam pencocokan urutan dan dengan pencocokan urutan:

CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);

Mungkin terlalu khusus. Tetapi gunakan itu jika membaca kinerja untuk permintaan tertentu sangat penting. Jika ada DESC NULLS LASTdalam kueri, gunakan yang sama dalam indeks sehingga urutan sortir cocok dan indeks berlaku.

Efektivitas / Optimalisasi kinerja

Timbang biaya dan manfaat sebelum membuat indeks khusus untuk setiap permintaan. Potensi indeks di atas sangat tergantung pada distribusi data .

Indeks digunakan karena memberikan data yang diurutkan. Di Postgres 9.2 atau yang lebih baru, kueri juga dapat memanfaatkan pemindaian hanya indeks jika indeks lebih kecil dari tabel yang mendasarinya. Indeks harus dipindai secara keseluruhan.

Tolok ukur

Saya memiliki patokan sederhana di sini yang sudah usang sekarang. Saya menggantinya dengan patokan terperinci dalam jawaban terpisah ini .

Erwin Brandstetter
sumber
28
Ini adalah jawaban yang bagus untuk sebagian besar ukuran basis data, tetapi saya ingin menunjukkan bahwa ketika Anda mendekati ~ juta baris DISTINCT ONmenjadi sangat lambat. Implementasi selalu mengurutkan seluruh tabel dan memindai untuk duplikat, mengabaikan semua indeks (bahkan jika Anda telah membuat indeks multi-kolom yang diperlukan). Lihat answerextended.com/2009/05/03/postgresql-optimizing-distinct untuk solusi yang memungkinkan.
Meekohi
14
Menggunakan tata cara untuk "membuat kode lebih pendek" adalah ide yang buruk. Bagaimana dengan membiarkan nama kolom agar dapat dibaca?
KOTJMF
13
@ KOTJMF: Saya sarankan Anda pergi dengan preferensi pribadi Anda. Saya menunjukkan kedua opsi untuk mendidik. Sintaksis steno dapat berguna untuk ekspresi panjang dalam SELECTdaftar.
Erwin Brandstetter
1
@jangorecki: Tolok ukur asli adalah dari 2011, saya tidak memiliki pengaturan lagi. Tapi sudah waktunya untuk menjalankan tes dengan pg 9,4 dan pg 9,5 pula. Lihat detail dalam jawaban yang ditambahkan. . Anda dapat menambahkan komentar dengan hasil dari instalasi Anda di bawah?
Erwin Brandstetter
2
@ PirateApp: Bukan dari atas kepala saya. DISTINCT ONhanya baik untuk mendapatkan satu baris per grup rekan.
Erwin Brandstetter
134

Tolok ukur

Menguji kandidat yang paling menarik dengan Postgres 9.4 dan 9.5 dengan meja setengah realistis 200k baris di purchasesdan 10k yang berbedacustomer_id ( rata-rata. 20 baris per pelanggan ).

Untuk Postgres 9.5 saya menjalankan tes ke-2 dengan 86446 pelanggan yang berbeda secara efektif. Lihat di bawah ( rata-rata 2.3 baris per pelanggan ).

Mendirikan

Meja utama

CREATE TABLE purchases (
  id          serial
, customer_id int  -- REFERENCES customer
, total       int  -- could be amount of money in Cent
, some_column text -- to make the row bigger, more realistic
);

Saya menggunakan serial(batasan PK yang ditambahkan di bawah) dan bilangan bulat customer_idkarena itu pengaturan yang lebih umum. Juga ditambahkan some_columnuntuk menebus kolom biasanya lebih banyak.

Data dummy, PK, indeks - tabel khas juga memiliki beberapa tupel mati:

INSERT INTO purchases (customer_id, total, some_column)    -- insert 200k rows
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,200000) g;

ALTER TABLE purchases ADD CONSTRAINT purchases_id_pkey PRIMARY KEY (id);

DELETE FROM purchases WHERE random() > 0.9; -- some dead rows

INSERT INTO purchases (customer_id, total, some_column)
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,20000) g;  -- add 20k to make it ~ 200k

CREATE INDEX purchases_3c_idx ON purchases (customer_id, total DESC, id);

VACUUM ANALYZE purchases;

customer tabel - untuk kueri superior

CREATE TABLE customer AS
SELECT customer_id, 'customer_' || customer_id AS customer
FROM   purchases
GROUP  BY 1
ORDER  BY 1;

ALTER TABLE customer ADD CONSTRAINT customer_customer_id_pkey PRIMARY KEY (customer_id);

VACUUM ANALYZE customer;

Dalam pengujian kedua saya untuk 9,5 saya menggunakan setup yang sama, tetapi dengan random() * 100000menghasilkan customer_idhanya beberapa baris per customer_id.

Ukuran objek untuk tabel purchases

Dihasilkan dengan kueri ini .

               what                | bytes/ct | bytes_pretty | bytes_per_row
-----------------------------------+----------+--------------+---------------
 core_relation_size                | 20496384 | 20 MB        |           102
 visibility_map                    |        0 | 0 bytes      |             0
 free_space_map                    |    24576 | 24 kB        |             0
 table_size_incl_toast             | 20529152 | 20 MB        |           102
 indexes_size                      | 10977280 | 10 MB        |            54
 total_size_incl_toast_and_indexes | 31506432 | 30 MB        |           157
 live_rows_in_text_representation  | 13729802 | 13 MB        |            68
 ------------------------------    |          |              |
 row_count                         |   200045 |              |
 live_tuples                       |   200045 |              |
 dead_tuples                       |    19955 |              |

Pertanyaan

1. row_number()di CTE, ( lihat jawaban lain )

WITH cte AS (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   )
SELECT id, customer_id, total
FROM   cte
WHERE  rn = 1;

2. row_number()di subquery (optimasi saya)

SELECT id, customer_id, total
FROM   (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   ) sub
WHERE  rn = 1;

3. DISTINCT ON( lihat jawaban lain )

SELECT DISTINCT ON (customer_id)
       id, customer_id, total
FROM   purchases
ORDER  BY customer_id, total DESC, id;

4. rCTE dengan LATERALsubquery ( lihat di sini )

WITH RECURSIVE cte AS (
   (  -- parentheses required
   SELECT id, customer_id, total
   FROM   purchases
   ORDER  BY customer_id, total DESC
   LIMIT  1
   )
   UNION ALL
   SELECT u.*
   FROM   cte c
   ,      LATERAL (
      SELECT id, customer_id, total
      FROM   purchases
      WHERE  customer_id > c.customer_id  -- lateral reference
      ORDER  BY customer_id, total DESC
      LIMIT  1
      ) u
   )
SELECT id, customer_id, total
FROM   cte
ORDER  BY customer_id;

5. customertabel dengan LATERAL( lihat di sini )

SELECT l.*
FROM   customer c
,      LATERAL (
   SELECT id, customer_id, total
   FROM   purchases
   WHERE  customer_id = c.customer_id  -- lateral reference
   ORDER  BY total DESC
   LIMIT  1
   ) l;

6. array_agg()dengan ORDER BY( lihat jawaban lain )

SELECT (array_agg(id ORDER BY total DESC))[1] AS id
     , customer_id
     , max(total) AS total
FROM   purchases
GROUP  BY customer_id;

Hasil

Waktu eksekusi untuk kueri di atas dengan EXPLAIN ANALYZE(dan semua opsi tidak aktif ), terbaik dari 5 berjalan .

Semua pertanyaan menggunakan Pemindaian Hanya Indeks aktif purchases2_3c_idx(di antara langkah-langkah lain). Beberapa dari mereka hanya untuk ukuran indeks yang lebih kecil, yang lain lebih efektif.

A. Postgres 9.4 dengan 200.000 baris dan ~ 20 per customer_id

1. 273.274 ms  
2. 194.572 ms  
3. 111.067 ms  
4.  92.922 ms  
5.  37.679 ms  -- winner
6. 189.495 ms

B. Sama dengan Postgres 9.5

1. 288.006 ms
2. 223.032 ms  
3. 107.074 ms  
4.  78.032 ms  
5.  33.944 ms  -- winner
6. 211.540 ms  

C. Sama seperti B., tetapi dengan ~ 2,3 baris per customer_id

1. 381.573 ms
2. 311.976 ms
3. 124.074 ms  -- winner
4. 710.631 ms
5. 311.976 ms
6. 421.679 ms

Tolok ukur terkait

Inilah yang baru dengan pengujian "ogr" dengan baris 10M dan "pelanggan" 60r unik di Postgres 11.5 (saat ini pada September 2019). Hasil masih sejalan dengan apa yang telah kita lihat sejauh ini:

Benchmark asli (kedaluwarsa) dari 2011

Saya menjalankan tiga tes dengan PostgreSQL 9.1 pada tabel kehidupan nyata dari 65579 baris dan indeks btree satu kolom pada masing-masing dari tiga kolom yang terlibat dan mengambil waktu eksekusi terbaik dari 5 berjalan.
Membandingkan permintaan pertama @OMGPonies ( A) dengan solusi di atasDISTINCT ON ( B):

  1. Pilih seluruh tabel, hasil dalam 5958 baris dalam kasus ini.

    A: 567.218 ms
    B: 386.673 ms
  2. Gunakan kondisi yang WHERE customer BETWEEN x AND ymenghasilkan 1000 baris.

    A: 249.136 ms
    B:  55.111 ms
  3. Pilih satu pelanggan dengan WHERE customer = x.

    A:   0.143 ms
    B:   0.072 ms

Tes yang sama diulang dengan indeks yang dijelaskan dalam jawaban lainnya

CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);

1A: 277.953 ms  
1B: 193.547 ms

2A: 249.796 ms -- special index not used  
2B:  28.679 ms

3A:   0.120 ms  
3B:   0.048 ms
Erwin Brandstetter
sumber
5
Terima kasih atas tolok ukur yang bagus. Saya bertanya-tanya apakah kueri data peristiwa di mana Anda memiliki stempel waktu dan bukan total akan mendapat manfaat dari indeks BRIN baru. Ini berpotensi dapat memberikan peningkatan untuk permintaan temporal.
jangorecki
3
@jangorecki: Setiap tabel besar dengan data yang diurutkan secara fisik dapat mengambil manfaat dari indeks BRIN.
Erwin Brandstetter
@ ErwinBrandstetter Dalam 2. row_number()dan 5. customer table with LATERALcontoh, apa yang memastikan id akan menjadi yang terkecil?
Artem Novikov
@ ArtemNovikov: Tidak ada. Tujuannya adalah untuk mengambil, per customer_id baris dengan yang tertinggi total. Ini adalah kebetulan yang menyesatkan dalam data uji dari pertanyaan bahwa iddalam baris yang dipilih kebetulan juga menjadi per terkecil customer_id.
Erwin Brandstetter
1
@ArtemNovikov: Untuk memungkinkan pemindaian hanya indeks.
Erwin Brandstetter
55

Ini biasa masalah, yang sudah memiliki solusi yang teruji dan sangat optimal . Secara pribadi saya lebih suka solusi join kiri oleh Bill Karwin ( posting asli dengan banyak solusi lain ).

Perhatikan bahwa banyak solusi untuk masalah umum ini secara mengejutkan dapat ditemukan di salah satu sumber paling resmi, manual MySQL ! Lihat Contoh Permintaan Umum :: Baris Memegang Maksimum Grup dari Kolom Tertentu .

TMS
sumber
22
Bagaimana manual MySQL dengan cara apa pun "resmi" untuk pertanyaan Postgres / SQLite (belum lagi SQL)? Juga, untuk menjadi jelas, DISTINCT ONversi ini jauh lebih pendek, lebih sederhana dan umumnya berkinerja lebih baik di Postgres daripada alternatif dengan self LEFT JOINatau semi-anti-join NOT EXISTS. Ini juga "diuji dengan baik".
Erwin Brandstetter
3
Selain apa yang ditulis Erwin, saya akan mengatakan bahwa menggunakan fungsi jendela (yang merupakan fungsi SQL yang umum saat ini) hampir selalu lebih cepat daripada menggunakan gabungan dengan tabel turunan
a_horse_with_no_name
6
Referensi yang bagus. Saya tidak tahu ini disebut masalah terbesar-n-per-kelompok. Terima kasih.
David Mann
Pertanyaannya bukan untuk n terbesar per grup tetapi yang pertama n.
reinierpost
1
Dalam kasus dua bidang pesanan saya mencoba, "solusi join kiri oleh Bill Karwin" memberikan kinerja yang buruk. Lihat komentar saya di bawah ini stackoverflow.com/a/8749095/684229
Johnny Wong
30

Di Postgres, Anda dapat menggunakan array_aggseperti ini:

SELECT  customer,
        (array_agg(id ORDER BY total DESC))[1],
        max(total)
FROM purchases
GROUP BY customer

Ini akan memberi Anda id pembelian terbesar setiap pelanggan.

Beberapa hal yang perlu diperhatikan:

  • array_aggadalah fungsi agregat, jadi itu berfungsi dengan GROUP BY.
  • array_aggmemungkinkan Anda menentukan pemesanan yang mencakup hanya untuk dirinya sendiri, sehingga tidak membatasi struktur dari keseluruhan kueri. Ada juga sintaks untuk bagaimana Anda mengurutkan NULLs, jika Anda perlu melakukan sesuatu yang berbeda dari default.
  • Setelah kita membangun array, kita ambil elemen pertama. (Array Postgres adalah 1-diindeks, bukan 0-diindeks).
  • Anda bisa menggunakan array_aggcara serupa untuk kolom output ketiga Anda, tetapimax(total) lebih sederhana.
  • Tidak seperti DISTINCT ONmenggunakan menggunakan array_aggmemungkinkan Anda menyimpannya GROUP BY, jika Anda menginginkannya karena alasan lain.
Paul A Jungwirth
sumber
14

Solusinya tidak terlalu efisien seperti yang ditunjukkan oleh Erwin, karena keberadaan SubQ

select * from purchases p1 where total in
(select max(total) from purchases where p1.customer=customer) order by total desc;
pengguna2407394
sumber
Terima kasih, ya setuju dengan Anda, bergabung antara subq dan permintaan luar sebenarnya membutuhkan waktu lebih lama. "Masuk" tidak akan menjadi masalah di sini karena subq hanya akan menghasilkan satu baris. BTW, kesalahan sintaks apa yang Anda tunjuk ??
user2407394
ohh .. dulu "Teradata" .. diedit sekarang..tetapi putusnya ikatan tidak diperlukan di sini karena harus mencari total tertinggi untuk setiap pelanggan ..
user2407394
Anda sadar bahwa Anda mendapatkan beberapa baris untuk satu pelanggan dalam kasus dasi? Apakah itu diinginkan tergantung pada persyaratan yang tepat. Biasanya tidak. Untuk pertanyaan yang ada, judulnya cukup jelas.
Erwin Brandstetter
Ini tidak jelas dari pertanyaan, jika pelanggan yang sama telah membeli = Maks untuk 2 id berbeda, saya pikir kita harus menampilkan keduanya.
user2407394
10

Saya menggunakan cara ini (hanya postgresql): https://wiki.postgresql.org/wiki/First/last_%28aggregate%29

-- Create a function that always returns the first non-NULL item
CREATE OR REPLACE FUNCTION public.first_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $1;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.first (
        sfunc    = public.first_agg,
        basetype = anyelement,
        stype    = anyelement
);

-- Create a function that always returns the last non-NULL item
CREATE OR REPLACE FUNCTION public.last_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $2;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.last (
        sfunc    = public.last_agg,
        basetype = anyelement,
        stype    = anyelement
);

Maka contoh Anda harus bekerja hampir seperti:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY FIRST(total) DESC;

CAVEAT: Itu mengabaikan baris NULL


Sunting 1 - Gunakan ekstensi postgres sebagai gantinya

Sekarang saya menggunakan cara ini: http://pgxn.org/dist/first_last_agg/

Untuk menginstal di ubuntu 14.04:

apt-get install postgresql-server-dev-9.3 git build-essential -y
git clone git://github.com/wulczer/first_last_agg.git
cd first_last_app
make && sudo make install
psql -c 'create extension first_last_agg'

Ini adalah ekstensi postgres yang memberi Anda fungsi pertama dan terakhir; ternyata lebih cepat dari cara di atas.


Sunting 2 - Memesan dan memfilter

Jika Anda menggunakan fungsi agregat (seperti ini), Anda dapat memesan hasilnya, tanpa harus memiliki data yang sudah dipesan:

http://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES

Jadi contoh yang setara, dengan pemesanan akan menjadi sesuatu seperti:

SELECT first(id order by id), customer, first(total order by id)
  FROM purchases
 GROUP BY customer
 ORDER BY first(total);

Tentu saja Anda dapat memesan dan memfilter sesuai dengan agregat; itu sintaks yang sangat kuat.

matiu
sumber
Menggunakan pendekatan fungsi khusus ini juga. Cukup universal dan sederhana. Mengapa menyulitkan, apakah ini jauh lebih sedikit solusi daripada yang lain?
Sergey Shcherbakov
9

Pertanyaan:

SELECT purchases.*
FROM purchases
LEFT JOIN purchases as p 
ON 
  p.customer = purchases.customer 
  AND 
  purchases.total < p.total
WHERE p.total IS NULL

BAGAIMANA CARA KERJANYA! (Aku pernah disana)

Kami ingin memastikan bahwa kami hanya memiliki total tertinggi untuk setiap pembelian.


Beberapa Hal Teoritis (lewati bagian ini jika Anda hanya ingin memahami permintaan)

Biarkan Total menjadi fungsi T (pelanggan, id) di mana ia mengembalikan nilai yang diberikan nama dan id Untuk membuktikan bahwa total yang diberikan (T (pelanggan, id)) adalah yang tertinggi kita harus membuktikan bahwa Kami ingin membuktikan

  • ∀x T (pelanggan, id)> T (pelanggan, x) (total ini lebih tinggi dari semua total lainnya untuk pelanggan itu)

ATAU

  • ¬∃x T (pelanggan, id) <T (pelanggan, x) (tidak ada total yang lebih tinggi untuk pelanggan itu)

Pendekatan pertama akan membutuhkan kita untuk mendapatkan semua catatan untuk nama yang tidak terlalu saya sukai.

Yang kedua akan membutuhkan cara yang cerdas untuk mengatakan tidak ada catatan yang lebih tinggi dari yang ini.


Kembali ke SQL

Jika kita pergi, gabungkan tabel dengan nama dan totalnya kurang dari tabel yang digabungkan:

      LEFT JOIN purchases as p 
      ON 
      p.customer = purchases.customer 
      AND 
      purchases.total < p.total

kami memastikan bahwa semua catatan yang memiliki catatan lain dengan total lebih tinggi untuk pengguna yang sama untuk bergabung:

purchases.id, purchases.customer, purchases.total, p.id, p.customer, p.total
1           , Tom           , 200             , 2   , Tom   , 300
2           , Tom           , 300
3           , Bob           , 400             , 4   , Bob   , 500
4           , Bob           , 500
5           , Alice         , 600             , 6   , Alice   , 700
6           , Alice         , 700

Itu akan membantu kami memfilter untuk total tertinggi untuk setiap pembelian tanpa pengelompokan yang diperlukan:

WHERE p.total IS NULL

purchases.id, purchases.name, purchases.total, p.id, p.name, p.total
2           , Tom           , 300
4           , Bob           , 500
6           , Alice         , 700

Dan itulah jawaban yang kita butuhkan.

khaled_gomaa
sumber
8

Solusi yang sangat cepat

SELECT a.* 
FROM
    purchases a 
    JOIN ( 
        SELECT customer, min( id ) as id 
        FROM purchases 
        GROUP BY customer 
    ) b USING ( id );

dan sangat cepat jika tabel diindeks oleh id:

create index purchases_id on purchases (id);
Alejandro Salamanca Mazuelo
sumber
Klausa USING sangat standar. Hanya saja beberapa sistem basis data kecil tidak memilikinya.
Holger Jakobs
2
Ini tidak menemukan pembelian pelanggan dengan total terbesar
Johnny Wong
7

Di SQL Server Anda bisa melakukan ini:

SELECT *
FROM (
SELECT ROW_NUMBER()
OVER(PARTITION BY customer
ORDER BY total DESC) AS StRank, *
FROM Purchases) n
WHERE StRank = 1

Penjelasan: Here Group by dilakukan berdasarkan pelanggan dan kemudian memesannya secara total maka masing-masing grup tersebut diberi nomor seri sebagai StRank dan kami mengeluarkan 1 pelanggan pertama dengan StRank 1

Diwas Poudel
sumber
Terima kasih! Ini bekerja dengan sempurna dan sangat mudah dimengerti dan diimplementasikan.
ruohola
4

Di PostgreSQL, kemungkinan lain adalah menggunakan first_valuefungsi jendela dalam kombinasi dengan SELECT DISTINCT:

select distinct customer_id,
                first_value(row(id, total)) over(partition by customer_id order by total desc, id)
from            purchases;

Saya membuat komposit (id, total), jadi kedua nilai dikembalikan oleh agregat yang sama. Tentu saja Anda selalu dapat menerapkan first_value()dua kali.

pbillen
sumber
3

Solusi OMG Ponies "Didukung oleh basis data apa pun" yang diterima memiliki kecepatan yang baik dari pengujian saya.

Di sini saya memberikan pendekatan yang sama, tetapi solusi database apa pun lebih lengkap dan bersih. Dasi dipertimbangkan (anggap keinginan untuk mendapatkan hanya satu baris untuk setiap pelanggan, bahkan beberapa catatan untuk jumlah maksimum per pelanggan), dan bidang pembelian lainnya (mis. Pembelian_payment_id) akan dipilih untuk baris yang benar-benar cocok di tabel pembelian.

Didukung oleh basis data apa pun:

select * from purchase
join (
    select min(id) as id from purchase
    join (
        select customer, max(total) as total from purchase
        group by customer
    ) t1 using (customer, total)
    group by customer
) t2 using (id)
order by customer

Permintaan ini cukup cepat terutama ketika ada indeks komposit seperti (pelanggan, total) pada tabel pembelian.

Ucapan:

  1. t1, t2 adalah alias subquery yang dapat dihapus tergantung pada database.

  2. Peringatan : using (...)klausa saat ini tidak didukung dalam MS-SQL dan Oracle db pada edit ini pada Januari 2017. Anda harus mengembangkannya sendiri ke mis on t2.id = purchase.id. Dll. Sintaks PENGGUNAAN bekerja dalam SQLite, MySQL dan PostgreSQL.

Johnny Wong
sumber
2

Snowflake / Teradata mendukung QUALIFYklausa yang berfungsi seperti HAVINGuntuk fungsi berjendela:

SELECT id, customer, total
FROM PURCHASES
QUALIFY ROW_NUMBER() OVER(PARTITION BY p.customer ORDER BY p.total DESC) = 1
Lukasz Szozda
sumber
1
  • Jika Anda ingin memilih baris apa pun (berdasarkan kondisi spesifik Anda) dari kumpulan baris teragregasi.

  • Jika Anda ingin menggunakan sum/avgfungsi agregasi ( ) lainnya sebagai tambahan max/min. Dengan demikian Anda tidak dapat menggunakan petunjukDISTINCT ON

Anda dapat menggunakan subquery berikutnya:

SELECT  
    (  
       SELECT **id** FROM t2   
       WHERE id = ANY ( ARRAY_AGG( tf.id ) ) AND amount = MAX( tf.amount )   
    ) id,  
    name,   
    MAX(amount) ma,  
    SUM( ratio )  
FROM t2  tf  
GROUP BY name

Anda bisa mengganti amount = MAX( tf.amount ) dengan kondisi apa pun yang Anda inginkan dengan satu batasan: Subquery ini tidak boleh mengembalikan lebih dari satu baris

Tetapi jika Anda ingin melakukan hal-hal seperti itu, Anda mungkin mencari fungsi jendela

Eugen Konkov
sumber
1

Untuk SQl Server cara yang paling efisien adalah:

with
ids as ( --condition for split table into groups
    select i from (values (9),(12),(17),(18),(19),(20),(22),(21),(23),(10)) as v(i) 
) 
,src as ( 
    select * from yourTable where  <condition> --use this as filter for other conditions
)
,joined as (
    select tops.* from ids 
    cross apply --it`s like for each rows
    (
        select top(1) * 
        from src
        where CommodityId = ids.i 
    ) as tops
)
select * from joined

dan jangan lupa untuk membuat indeks berkerumun untuk kolom yang digunakan

BazSTR
sumber