PostgreSQL - bir sütun için maksimum değere sahip satırı getir


99

Time_stamp, usr_id, transaction_id ve life_remaining için sütunlara sahip kayıtları içeren bir Postgres tablosu ("yaşamlar" olarak adlandırılır) ile uğraşıyorum. Her usr_id için bana en son yaşamları geri kalan toplamı verecek bir sorguya ihtiyacım var

  1. Birden fazla kullanıcı var (farklı usr_id'ler)
  2. time_stamp benzersiz bir tanımlayıcı değildir: bazen kullanıcı olayları (tabloda tek tek) aynı time_stamp ile gerçekleşir.
  3. trans_id yalnızca çok küçük zaman aralıkları için benzersizdir: zamanla tekrar eder
  4. Kalan_ yaşamlar (belirli bir kullanıcı için) zaman içinde hem artabilir hem de azalabilir

misal:

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  07:00 | 1 | 1 | 1    
  09:00 | 4 | 2 | 2    
  10:00 | 2 | 3 | 3    
  10:00 | 1 | 2 | 4    
  11:00 | 4 | 1 | 5    
  11:00 | 3 | 1 | 6    
  13:00 | 3 | 3 | 1    

Verilen her usr_id için en son verileri içeren satırın diğer sütunlarına erişmem gerekeceğinden, şöyle bir sonuç veren bir sorguya ihtiyacım var:

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  11:00 | 3 | 1 | 6    
  10:00 | 1 | 2 | 4    
  13:00 | 3 | 3 | 1    

Belirtildiği gibi, her usr_id can kazanabilir veya kaybedebilir ve bazen bu zaman damgalı olaylar, aynı zaman damgasına sahip olacak kadar birbirine çok yakın gerçekleşir! Bu nedenle bu sorgu çalışmayacaktır:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp) AS max_timestamp 
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp = b.time_stamp

Bunun yerine, doğru satırı belirlemek için hem time_stamp (birinci) hem de trans_id (saniye) kullanmam gerekiyor. Daha sonra bu bilgiyi alt sorgudan, uygun satırların diğer sütunları için veri sağlayacak olan ana sorguya aktarmam gerekiyor. Bu, çalışmak zorunda olduğum saldırıya uğramış sorgu:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp || '*' || trans_id) 
       AS max_timestamp_transid
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp_transid = b.time_stamp || '*' || b.trans_id 
ORDER BY b.usr_id

Tamam, bu işe yarıyor ama hoşuma gitmiyor. Sorgu içinde bir sorgu, kendi kendine birleştirme gerektirir ve bana öyle geliyor ki, MAX'ın en büyük zaman damgasına ve trans_id'ye sahip olduğunu bulduğu satırı yakalayarak çok daha basit olabilir. "Yaşıyor" tablosunun ayrıştırılması gereken on milyonlarca satırı var, bu nedenle bu sorgunun olabildiğince hızlı ve verimli olmasını istiyorum. Özellikle RDBM ve Postgres konusunda yeniyim, bu nedenle uygun dizinleri etkili bir şekilde kullanmam gerektiğini biliyorum. Nasıl optimize edeceğim konusunda biraz kayboldum.

Burada benzer bir tartışma buldum . Oracle analitik işlevine eşdeğer bir tür Postgres gerçekleştirebilir miyim?

Bir toplama işlevi (MAX gibi) tarafından kullanılan ilgili sütun bilgilerine erişme, dizinler oluşturma ve daha iyi sorgular oluşturma konusunda herhangi bir tavsiye çok takdir edilecektir!

Not: Örnek vakamı oluşturmak için aşağıdakileri kullanabilirsiniz:

create TABLE lives (time_stamp timestamp, lives_remaining integer, 
                    usr_id integer, trans_id integer);
insert into lives values ('2000-01-01 07:00', 1, 1, 1);
insert into lives values ('2000-01-01 09:00', 4, 2, 2);
insert into lives values ('2000-01-01 10:00', 2, 3, 3);
insert into lives values ('2000-01-01 10:00', 1, 2, 4);
insert into lives values ('2000-01-01 11:00', 4, 1, 5);
insert into lives values ('2000-01-01 11:00', 3, 1, 6);
insert into lives values ('2000-01-01 13:00', 3, 3, 1);

Josh, sorgunun kendiliğinden birleşmesinden hoşlanmayabilirsiniz, ancak RDBMS söz konusu olduğunda sorun değil.
vladr

1
Kendiliğinden birleşmenin gerçekte dönüştüğü şey, basit bir dizin eşlemesidir, burada iç SELECT (MAX olanı) ilgisiz girişleri atarak dizini tarar ve dış SELECT'in tablodaki sütunların geri kalanını yakaladığı yer. daraltılmış dizine karşılık gelir.
vladr

Vlad, ipuçları ve açıklama için teşekkürler. Veritabanının iç işleyişini anlamaya nasıl başlayacağımı ve sorguları nasıl optimize edeceğimi gözlerimi açtı. Quassnoi, harika sorgu ve birincil anahtarla ilgili ipucu için teşekkürler; Bill de. Çok yararlı.
Joshua Berry

Nasıl MAX BY2 sütun alacağımı bana gösterdiğin için teşekkür ederim !

Yanıtlar:


93

158 bin sözde rasgele sıralı bir tabloda (usr_id 0 ile 10k trans_idarasında eşit olarak dağıtılmış, 0 ile 30 arasında eşit olarak dağıtılmış),

Aşağıda, sorgu maliyeti olarak, xxx_costgerekli G / Ç ve CPU kaynaklarının ağırlıklı fonksiyon tahmini olan Postgres'in maliyet tabanlı optimize edicinin maliyet tahminine (Postgres'in varsayılan değerleriyle) atıfta bulunuyorum ; bunu PgAdminIII'yi çalıştırarak ve "Sorgu / Açıklama seçenekleri" "Analiz" olarak ayarlanmış sorgu üzerinde "Sorgu / Açıkla (F7)" çalıştırarak elde edebilirsiniz.

  • Quassnoy sorgusu 1.3 saniyede bir maliyet 745K tahminini (!) Ve tamamlanana vardır (bir bileşik endeks verilen ( usr_id, trans_id, time_stamp))
  • Bill'in sorgusunun maliyet tahmini 93k'dir ve 2,9 saniyede tamamlanır (( usr_id, trans_id) üzerinde bir bileşik indeks verildiğinde )
  • Sorgu 1. Aşağıda 16k olan bir maliyet tahmini vardır ve 800ms içinde tamamlamalar (bir bileşik endeksi verilen ( usr_id, trans_id, time_stamp))
  • Sorgu 2. Aşağıda 14k bir maliyet tahmini vardır ve 800ms içinde tamamlamalar (bir bileşik fonksiyon indeksi verilen ( usr_id, EXTRACT(EPOCH FROM time_stamp), trans_id))
    • bu Postgres'e özgüdür
  • Aşağıdaki sorgu 3. (Postgres 8.4+) sorgu 2. karşılaştırılabilir (ya da daha fazla) bir maliyet tahmini ve süresine sahip (bir bileşiğin göstergesi (verilen usr_id, time_stamp, trans_id)); livestabloyu yalnızca bir kez tarama avantajına sahiptir ve bellekteki sıralamayı barındırmak için geçici olarak (gerekirse) work_mem'i artırmanız durumunda , tüm sorgular arasında açık ara en hızlısı olacaktır.

Yukarıdaki tüm zamanlar, 10.000 satırlık sonuç kümesinin tamamının alınmasını içerir.

Hedefiniz , tahmini maliyete vurgu yaparak minimum maliyet tahmini ve minimum sorgu yürütme süresidir. Sorgu yürütme, çalışma zamanı koşullarına önemli ölçüde bağlı olabilir (örneğin, ilgili satırların bellekte tam olarak önbelleğe alınıp alınmadığı), ancak maliyet tahmini değildir. Öte yandan, maliyet tahmininin tam olarak bir tahmin olduğunu unutmayın.

En iyi sorgu yürütme süresi, özel bir veri tabanında yük olmadan çalıştırıldığında elde edilir (örneğin, bir geliştirme bilgisayarında pgAdminIII ile oynanır.) Sorgu süresi, üretimde gerçek makine yüküne / veri erişim dağılımına bağlı olarak değişiklik gösterir. Bir sorgu diğerinden biraz daha hızlı göründüğünde (<% 20) ancak çok daha yüksek bir maliyete sahip olduğunda, genellikle daha yüksek yürütme süresi ancak daha düşük maliyetli olanı seçmek daha akıllıca olacaktır.

Sorgu çalıştırıldığında üretim makinenizde bellek için bir rekabet olmayacağını düşündüğünüzde (örneğin, RDBMS önbelleği ve dosya sistemi önbelleği eşzamanlı sorgular ve / veya dosya sistemi etkinliği tarafından atılmayacaktır), o zaman elde ettiğiniz sorgu süresi bağımsız (örneğin bir geliştirme PC'sinde pgAdminIII) ​​modu temsilci olacaktır. Üretim sisteminde çakışması durumunda daha düşük maliyet ile Sorgu önbellek kadar dayanmaz olarak, sorgu zaman, tahmin edilen maliyet oranına orantılı olarak düşer , oysa daha yüksek bir maliyetle sorgu ve üzerinde aynı verileri tekrar olacaktır (tetikleme kararlı bir önbelleğin olmadığı durumlarda ek G / Ç), örneğin:

              cost | time (dedicated machine) |     time (under load) |
-------------------+--------------------------+-----------------------+
some query A:   5k | (all data cached)  900ms | (less i/o)     1000ms |
some query B:  50k | (all data cached)  900ms | (lots of i/o) 10000ms |

ANALYZE livesGerekli endeksleri oluşturduktan sonra bir kez çalıştırmayı unutmayın .


Sorgu 1

-- incrementally narrow down the result set via inner joins
--  the CBO may elect to perform one full index scan combined
--  with cascading index lookups, or as hash aggregates terminated
--  by one nested index lookup into lives - on my machine
--  the latter query plan was selected given my memory settings and
--  histogram
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
    SELECT
      usr_id,
      MAX(time_stamp) AS time_stamp_max
     FROM
      lives
     GROUP BY
      usr_id
  ) AS l2
 ON
  l1.usr_id     = l2.usr_id AND
  l1.time_stamp = l2.time_stamp_max
 INNER JOIN (
    SELECT
      usr_id,
      time_stamp,
      MAX(trans_id) AS trans_max
     FROM
      lives
     GROUP BY
      usr_id, time_stamp
  ) AS l3
 ON
  l1.usr_id     = l3.usr_id AND
  l1.time_stamp = l3.time_stamp AND
  l1.trans_id   = l3.trans_max

Sorgu 2

-- cheat to obtain a max of the (time_stamp, trans_id) tuple in one pass
-- this results in a single table scan and one nested index lookup into lives,
--  by far the least I/O intensive operation even in case of great scarcity
--  of memory (least reliant on cache for the best performance)
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
   SELECT
     usr_id,
     MAX(ARRAY[EXTRACT(EPOCH FROM time_stamp),trans_id])
       AS compound_time_stamp
    FROM
     lives
    GROUP BY
     usr_id
  ) AS l2
ON
  l1.usr_id = l2.usr_id AND
  EXTRACT(EPOCH FROM l1.time_stamp) = l2.compound_time_stamp[1] AND
  l1.trans_id = l2.compound_time_stamp[2]

2013/01/29 güncellemesi

Son olarak, 8.4 sürümünden itibaren Postgres, Pencere İşlevini desteklemektedir, yani şu kadar basit ve verimli bir şey yazabilirsiniz:

Sorgu 3

-- use Window Functions
-- performs a SINGLE scan of the table
SELECT DISTINCT ON (usr_id)
  last_value(time_stamp) OVER wnd,
  last_value(lives_remaining) OVER wnd,
  usr_id,
  last_value(trans_id) OVER wnd
 FROM lives
 WINDOW wnd AS (
   PARTITION BY usr_id ORDER BY time_stamp, trans_id
   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
 );

(Usr_id, trans_id, times_tamp) üzerindeki bir bileşik indeksle, "CREATE INDEX life_blah_idx ON CAN (usr_id, trans_id, time_stamp)" gibi bir şey mi kastediyorsunuz? Yoksa her sütun için üç ayrı dizin oluşturmalı mıyım? "Btree KULLANIMI" varsayılanına bağlı kalmalıyım, değil mi?
Joshua Berry

1
İlk seçeneğe evet: Demek istediğim CREATE INDEX life_blah_idx ON life (usr_id, trans_id, time_stamp). :) Şerefe.
vladr

Maliyet karşılaştırmasını yaptığınız için bile teşekkürler vladr! Çok eksiksiz cevap!
Adam

@vladr Cevabına yeni rastladım. Sorgu 1'in maliyeti 16k ve sorgu 2'nin maliyeti 14k olduğundan biraz kafam karıştı. Ancak, tablonun ilerleyen kısımlarında sorgu 1'in maliyetinin 5 bin olduğunu ve sorgu 2'nin maliyetinin 50 bin olduğunu söylüyorsunuz. Peki hangi sorguyu kullanmak tercih edilir? :) teşekkürler
Houman

1
@Kave, tablo, OP'nin iki sorgusunu değil, bir örneği gösteren varsayımsal bir sorgu çifti içindir. Karışıklığı azaltmak için yeniden adlandırma.
vladr

82

Aşağıdakilere dayalı olarak temiz bir sürüm öneririmDISTINCT ON (bkz. Belgeler ):

SELECT DISTINCT ON (usr_id)
    time_stamp,
    lives_remaining,
    usr_id,
    trans_id
FROM lives
ORDER BY usr_id, time_stamp DESC, trans_id DESC;

6
Bu çok kısa ve sağlam bir cevap. Ayrıca iyi bir referansı var! Kabul edilen cevap bu olmalıdır.
Prakhar Agrawal

Bu, başka hiçbir şeyin yapamayacağı biraz farklı uygulamamda benim için işe yaradı. Daha fazla görünürlük için kesinlikle yukarı kaldırılmalıdır.
Jim Factor

8

İlişkili alt sorgular veya GROUP BY kullanmayan başka bir yöntem. PostgreSQL performans ayarlaması konusunda uzman değilim, bu yüzden hangisinin sizin için daha iyi çalıştığını görmek için hem bunu hem de diğer kişiler tarafından verilen çözümleri denemenizi öneririm.

SELECT l1.*
FROM lives l1 LEFT OUTER JOIN lives l2
  ON (l1.usr_id = l2.usr_id AND (l1.time_stamp < l2.time_stamp 
   OR (l1.time_stamp = l2.time_stamp AND l1.trans_id < l2.trans_id)))
WHERE l2.usr_id IS NULL
ORDER BY l1.usr_id;

Bunun trans_iden azından verilen herhangi bir değerde benzersiz olduğunu varsayıyorum time_stamp.


4

Mike Woodhouse'un bahsettiğiniz diğer sayfadaki cevabının tarzını beğendim . Maksimize edilen şey tek bir sütun olduğunda özellikle kısa ve özdür, bu durumda alt sorgu MAX(some_col)ve GROUP BYdiğer sütunları kullanabilir , ancak sizin durumunuzda maksimize edilecek 2 parçalı bir miktarınız varsa, yine de bunu kullanarak yapabilirsiniz. ORDER BYartı LIMIT 1bunun yerine (Quassnoi tarafından yapıldığı gibi):

SELECT * 
FROM lives outer
WHERE (usr_id, time_stamp, trans_id) IN (
    SELECT usr_id, time_stamp, trans_id
    FROM lives sq
    WHERE sq.usr_id = outer.usr_id
    ORDER BY trans_id, time_stamp
    LIMIT 1
)

Row-constructor sözdizimini kullanmayı WHERE (a, b, c) IN (subquery)güzel buluyorum çünkü gereken laf kalabalığı miktarını azaltıyor.


4

Aslında bu sorun için karmaşık bir çözüm var. Diyelim ki bir bölgedeki her ormanın en büyük ağacını seçmek istiyorsunuz.

SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
FROM tree JOIN forest ON (tree.forest = forest.id)
GROUP BY forest.id

Ağaçları ormanlara göre gruplandırdığınızda, sıralanmamış bir ağaç listesi olacaktır ve en büyüğünü bulmanız gerekecektir. Yapmanız gereken ilk şey, satırları boyutlarına göre sıralamak ve listenizden ilkini seçmektir. Verimsiz görünebilir, ancak milyonlarca satırınız varsa, JOIN's ve WHEREkoşulları içeren çözümlerden çok daha hızlı olacaktır .

BTW, ORDER_BYiçin array_aggPostgresql 9.0'da tanıtıldığını unutmayın


Bir hatanız var. ORDER BY tree_size.size DESC yazmanız gerekiyor. Ayrıca, yazarın görevi için kod şöyle görünecektir: SELECT usr_id, (array_agg(time_stamp ORDER BY time_stamp DESC))[1] AS timestamp, (array_agg(lives_remaining ORDER BY time_stamp DESC))[1] AS lives_remaining, (array_agg(trans_id ORDER BY time_stamp DESC))[1] AS trans_id FROM lives GROUP BY usr_id
alexkovelsky

3

Postgressql 9.5'te DISTINCT ON adında yeni bir seçenek var

SELECT DISTINCT ON (location) location, time, report
    FROM weather_reports
    ORDER BY location, time DESC;

Yinelenen satırları ortadan kaldırır ve ORDER BY yan tümcemde tanımlandığı gibi yalnızca ilk satırı bırakır.

resmi belgelere bakın


1
SELECT  l.*
FROM    (
        SELECT DISTINCT usr_id
        FROM   lives
        ) lo, lives l
WHERE   l.ctid = (
        SELECT ctid
        FROM   lives li
        WHERE  li.usr_id = lo.usr_id
        ORDER BY
          time_stamp DESC, trans_id DESC
        LIMIT 1
        )

Üzerinde bir dizin oluşturmak (usr_id, time_stamp, trans_id)bu sorguyu büyük ölçüde geliştirecektir.

Her zaman, her zaman PRIMARY KEYsofralarınızda bir çeşit bulundurmalısınız .


0

Sanırım burada büyük bir probleminiz var: belirli bir satırın diğerinden daha sonra gerçekleştiğini garanti edecek monoton bir şekilde artan "sayaç" yok. Bu örneği ele alalım:

timestamp   lives_remaining   user_id   trans_id
10:00       4                 3         5
10:00       5                 3         6
10:00       3                 3         1
10:00       2                 3         2

En son girdi olan bu veriden belirleyemezsiniz. İkincisi mi yoksa sonuncusu mu? Size doğru cevabı vermek için bu verilerin herhangi birine uygulayabileceğiniz sort veya max () işlevi yoktur.

Zaman damgasının çözünürlüğünü artırmak çok yardımcı olacaktır. Veritabanı motoru istekleri seri hale getirdiğinden, yeterli çözünürlükle iki zaman damgasının aynı olmayacağını garanti edebilirsiniz.

Alternatif olarak, çok çok uzun bir süre devredilmeyecek bir trans_id kullanın. Dönen bir trans_id'e sahip olmak, karmaşık bir matematik yapmadığınız sürece trans_id 6'nın trans_id 1'den daha yeni olup olmadığını söyleyemeyeceğiniz anlamına gelir (aynı zaman damgası için).


Evet, ideal olarak bir sıra (otomatik artış) sütunu sırayla olacaktır.
vladr

Yukarıdaki varsayım, küçük zaman artışları için trans_id'nin devredilmeyeceğiydi. Tablonun, yinelenmeyen bir trans_id gibi benzersiz bir birincil dizine ihtiyacı olduğunu kabul ediyorum. (Not: Artık yorum yapacak kadar karma / itibar puanım olduğu için mutluyum!)
Joshua Berry

Vlad, trans_id'in sık sık değişen oldukça kısa bir döngüye sahip olduğunu belirtir. Tablomdan sadece ortadaki iki satırı dikkate alsanız bile (trans_id = 6 ve 1), hangisinin en yeni olduğunu hala söyleyemezsiniz. Bu nedenle, belirli bir zaman damgası için max (trans_id) kullanmak işe yaramaz.
Barry Brown

Evet, uygulama yazarının (time_stamp, trans_id) demetinin belirli bir kullanıcı için benzersiz olduğuna dair garantisine güveniyorum. Durum böyle değilse, "SELECT l1.usr_id, l1.lives_left, ... FROM ... WHERE ...", "SELECT l1.usr_id, MAX / MIN (l1.lives_left), ... FROM olmalıdır. .. NEREDE ... GROUP BY l1.usr_id, ...
vladr

0

Yararlı bulabileceğiniz başka bir çözüm.

SELECT t.*
FROM
    (SELECT
        *,
        ROW_NUMBER() OVER(PARTITION BY usr_id ORDER BY time_stamp DESC) as r
    FROM lives) as t
WHERE t.r = 1
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.