Bagaimana cara mendapatkan nilai saat ini dan selanjutnya yang lebih besar dalam satu pilih?

18

Saya memiliki tabel 'idtimes' InnoDB (MySQL 5.0.22-log) dengan kolom

`id` int(11) NOT NULL,
`time` int(20) NOT NULL, [...]

dengan kunci unik majemuk

UNIQUE KEY `id_time` (`id`,`time`)

sehingga ada beberapa stempel waktu per id dan beberapa id per stempel waktu.

Saya mencoba mengatur kueri di mana saya mendapatkan semua entri plus waktu berikutnya yang lebih besar untuk setiap entri, jika ada, maka itu harus kembali misalnya:

+-----+------------+------------+
| id  | time       | nexttime   |
+-----+------------+------------+
| 155 | 1300000000 | 1311111111 |
| 155 | 1311111111 | 1322222222 |
| 155 | 1322222222 |       NULL |
| 156 | 1312345678 | 1318765432 |
| 156 | 1318765432 |       NULL |
+-----+------------+------------+

Saat ini saya sejauh ini:

SELECT l.id, l.time, r.time FROM 
    idtimes AS l LEFT JOIN idtimes AS r ON l.id = r.id
    WHERE l.time < r.time ORDER BY l.id ASC, l.time ASC;

tapi tentu saja ini mengembalikan semua baris dengan r.time> l.time dan tidak hanya yang pertama ...

Saya kira saya perlu subselect seperti

SELECT outer.id, outer.time, 
    (SELECT time FROM idtimes WHERE id = outer.id AND time > outer.time 
        ORDER BY time ASC LIMIT 1)
    FROM idtimes AS outer ORDER BY outer.id ASC, outer.time ASC;

tapi saya tidak tahu bagaimana merujuk ke waktu saat ini (saya tahu di atas bukan SQL yang valid).

Bagaimana saya melakukan ini dengan satu query (dan saya lebih suka untuk tidak menggunakan @variables yang bergantung pada melangkah melalui tabel satu baris pada satu waktu dan mengingat nilai terakhir)?

Martin Hennings
sumber

Jawaban:

20

Melakukan GABUNG adalah satu hal yang mungkin Anda butuhkan.

SELECT l.id, l.time, r.time FROM 
    idtimes AS l LEFT JOIN idtimes AS r ON l.id = r.id

Saya kira join luarnya disengaja, dan Anda ingin mendapatkan nol. Lebih lanjut tentang itu nanti.

WHERE l.time < r.time ORDER BY l.id ASC, l.time ASC;

Anda hanya ingin r. baris yang memiliki waktu terendah (MIN) yang lebih tinggi dari waktu. Itu adalah tempat di mana Anda perlu subquerying.

WHERE r.time = (SELECT MIN(time) FROM idtimes r2 where r2.id = l.id AND r2.time > l.time)

Sekarang ke nol. Jika "tidak ada waktu berikutnya yang lebih tinggi", maka SELECT MIN () akan mengevaluasi menjadi nol (atau lebih buruk), dan itu sendiri tidak pernah sebanding dengan apa pun, sehingga klausa WHERE Anda tidak akan pernah terpuaskan, dan "waktu tertinggi" untuk setiap ID, tidak akan pernah muncul di set hasil.

Anda menyelesaikannya dengan menghapus GABUNGAN Anda, dan memindahkan subquery skalar ke daftar SELECT:

SELECT id, time, 
    (SELECT MIN(time) FROM idtimes sub 
        WHERE sub.id = main.id AND sub.time > main.time) as nxttime
  FROM idtimes AS main 
Erwin Smout
sumber
4

Saya selalu menghindari menggunakan subquery baik di SELECTblok atau di FROMblok, karena itu membuat kode "lebih kotor" dan kadang-kadang kurang efisien.

Saya pikir cara yang lebih elegan untuk melakukannya adalah dengan:

1. Temukan waktu yang lebih besar dari waktu baris

Anda dapat melakukan ini dengan tabel JOINantara waktu dengan sendirinya, membatasi gabungan ke id yang sama dan ke waktu yang lebih besar dari waktu baris saat ini.

Anda harus menggunakan LEFT JOINuntuk menghindari mengecualikan baris di mana tidak ada kali lebih besar dari salah satu baris saat ini.

SELECT
    i1.id,
    i1.time AS time,
    i2.time AS greater_time
FROM
    idtimes AS i1
    LEFT JOIN idtimes AS i2 ON i1.id = i2.id AND i2.time > i1.time

Masalahnya, seperti yang Anda sebutkan, adalah Anda memiliki beberapa baris di mana next_time lebih besar dari waktu .

+-----+------------+--------------+
| id  | time       | greater_time |
+-----+------------+--------------+
| 155 | 1300000000 | 1311111111   |
| 155 | 1300000000 | 1322222222   |
| 155 | 1311111111 | 1322222222   |
| 155 | 1322222222 |       NULL   |
| 156 | 1312345678 | 1318765432   |
| 156 | 1318765432 |       NULL   |
+-----+------------+--------------+

2. Temukan baris di mana Greater_time tidak hanya lebih besar tetapi Next_time

Cara terbaik untuk memfilter semua baris tidak berguna ini adalah untuk mengetahui apakah ada waktu antara waktu (lebih besar dari) dan lebih besar_waktu (lebih kecil dari) untuk id ini .

SELECT
    i1.id,
    i1.time AS time,
    i2.time AS next_time,
    i3.time AS intrudor_time
FROM
    idtimes AS i1
    LEFT JOIN idtimes AS i2 ON i1.id = i2.id AND i2.time > i1.time
    LEFT JOIN idtimes AS i3 ON i2.id = i3.id AND i3.time > i1.time AND i3.time < i2.time

ops, kami masih memiliki false_time berikutnya !

+-----+------------+--------------+---------------+
| id  | time       | next_time    | intrudor_time |
+-----+------------+--------------+---------------+
| 155 | 1300000000 | 1311111111   |         NULL  |
| 155 | 1300000000 | 1322222222   |    1311111111 |
| 155 | 1311111111 | 1322222222   |         NULL  |
| 155 | 1322222222 |       NULL   |         NULL  |
| 156 | 1312345678 | 1318765432   |         NULL  |
| 156 | 1318765432 |       NULL   |         NULL  |
+-----+------------+--------------+---------------+

Cukup filter baris di mana peristiwa ini terjadi, tambahkan WHEREkendala di bawah ini

WHERE
    i3.time IS NULL

Voa, kita memiliki apa yang kita butuhkan!

+-----+------------+--------------+---------------+
| id  | time       | next_time    | intrudor_time |
+-----+------------+--------------+---------------+
| 155 | 1300000000 | 1311111111   |         NULL  |
| 155 | 1311111111 | 1322222222   |         NULL  |
| 155 | 1322222222 |       NULL   |         NULL  |
| 156 | 1312345678 | 1318765432   |         NULL  |
| 156 | 1318765432 |       NULL   |         NULL  |
+-----+------------+--------------+---------------+

Saya harap Anda masih membutuhkan jawaban setelah 4 tahun!

luisfsns
sumber
Itu pintar. Saya tidak yakin itu lebih mudah dimengerti. Saya pikir jika kita mengganti is nulldan bergabung dengan i3 where not exists (select 1 from itimes i3 where [same clause]), maka kode akan lebih mencerminkan apa yang ingin kita ungkapkan.
Andrew Spencer
Terima kasih, Anda telah menyelamatkan hari (berikutnya) saya!
Jakob
2

Sebelum menyajikan solusi, saya harus perhatikan itu tidak cantik. Akan jauh lebih mudah jika Anda memiliki beberapa AUTO_INCREMENTkolom di meja Anda (bukan?)

SELECT 
  l.id, l.time, 
  SUBSTRING_INDEX(GROUP_CONCAT(r.time ORDER BY r.time), ',', 1)
FROM 
  idtimes AS l 
  LEFT JOIN idtimes AS r ON (l.id = r.id)
WHERE 
  l.time < r.time
GROUP BY
  l.id, l.time

Penjelasan:

  • Sama seperti milik Anda: gabung dua tabel, yang tepat hanya mendapat waktu yang lebih tinggi
  • KELOMPOK OLEH kedua kolom dari tabel kiri: ini memastikan kami mendapatkan semua (id, time)kombinasi (yang juga dikenal unik).
  • Untuk masing-masing (l.id, l.time), dapatkan yang pertama r.time lebih besar dari l.time. Ini terjadi dengan pertama memesan r.times via GROUP_CONCAT(r.time ORDER BY r.time), dengan mengiris token pertama melalui SUBSTRING_INDEX.

Semoga sukses, dan, jangan berharap kinerja yang baik jika tabel ini besar.

Shlomi Noach
sumber
2

Anda juga bisa mendapatkan apa yang Anda inginkan dari min()dan GROUP BYtanpa pilih dalam:

SELECT l.id, l.time, min(r.time) 
FROM idtimes l 
LEFT JOIN idtimes r on (r.id = l.id and r.time > l.time)
GROUP BY l.id, l.time;

Saya hampir akan bertaruh sejumlah besar uang bahwa pengoptimal mengubah ini menjadi hal yang sama dengan jawaban Erwin Smout, dan masih bisa diperdebatkan apakah itu lebih jelas, tetapi ada untuk kelengkapannya ...

Andrew Spencer
sumber
1
Untuk apa nilainya, SSMS & SQLServer 2016 lebih menyukai kueri Anda daripada Erwin (runtime 2s versus runtime 24 detik pada ~ 24k hasil yang ditetapkan)
Nathan Lafferty
Andrew sepertinya Anda telah kehilangan taruhan :-)
Erwin Smout
Menarik, karena seharusnya menjadi kasus umum bahwa subquery yang bergabung kembali ke tabel kueri luar oleh salah satu kolom PK sama dengan grup oleh. Saya ingin tahu apakah ada database lain yang akan mengoptimalkannya dengan lebih baik. (Saya tahu sedikit tentang pengoptimal basis data BTW; hanya ingin tahu.)
Andrew Spencer