“En son karşılık gelen satır” ı nasıl verimli bir şekilde alabilirim?


53

Çok yaygın olması gereken bir sorgu şablonuna sahibim, ancak bunun için nasıl verimli bir sorgu yazacağımı bilmiyorum. Başka bir tablonun satırlarının "en sonra çıkmadığı son tarih" e karşılık gelen bir tablonun satırlarına bakmak istiyorum.

inventoryBelirli bir günde sahip olduğum envanteri temsil eden bir masam var .

date       | good | quantity
------------------------------
2013-08-09 | egg  | 5
2013-08-09 | pear | 7
2013-08-02 | egg  | 1
2013-08-02 | pear | 2

ve belirli bir günde bir malın fiyatını tutan “fiyat” der ve

date       | good | price
--------------------------
2013-08-07 | egg  | 120
2013-08-06 | pear | 200
2013-08-01 | egg  | 110
2013-07-30 | pear | 220

Envanter tablosunun her bir satırı için "en son" fiyatı nasıl verimli bir şekilde alabilirim?

date       | pricing date | good | quantity | price
----------------------------------------------------
2013-08-09 | 2013-08-07   | egg  | 5        | 120
2013-08-09 | 2013-08-06   | pear | 7        | 200
2013-08-02 | 2013-08-01   | egg  | 1        | 110
2013-08-02 | 2013-07-30   | pear | 2        | 220

Bunu yapmanın bir yolunu biliyorum:

select inventory.date, max(price.date) as pricing_date, good
from inventory, price
where inventory.date >= price.date
and inventory.good = price.good
group by inventory.date, good

ve sonra bu sorguyu tekrar envantere katılın . Büyük tablolar için bile ilk sorguyu yapmak ( tekrar envantere katılmadan ) çok yavaştır. Bununla birlikte, programlama dilimi envanter tablosundan max(price.date) ... where price.date <= date_of_interest ... order by price.date desc limit 1her biri için bir sorgu yayınlamak için kullanırsam, aynı sorun hızlı bir şekilde çözülür date_of_interest, bu nedenle hesaplamalı bir engel olmadığını biliyorum. Bununla birlikte, tüm sorunu tek bir SQL sorgusu ile çözmeyi tercih ederim, çünkü bu sorgunun sonucu üzerinde daha fazla SQL işlem yapmamı sağlayacaktı.

Bunu verimli bir şekilde yapmanın standart bir yolu var mı? Sık sık gelmesi gerektiği ve bunun için hızlı bir sorgu yazmanın bir yolu olması gerektiğini düşünüyor.

Postgres kullanıyorum, ancak bir SQL-jenerik cevap takdir edilecektir.


3
Verimlilikle ilgili bir soru olarak DBA.SE'ye taşınmaya oy verildi. Sorguyu birkaç farklı yolla yazabiliriz ancak bu daha hızlı sonuç vermez.
ypercubeᵀᴹ 09:13

5
Bütün gün boyunca bütün mallara tek bir sorgudan mı ihtiyacınız var? Beklenmedik bir gereksinim gibi görünüyor? Daha yaygın olarak, belirli bir tarih için fiyatlar veya belirli bir mal için (belirli bir tarihte) fiyat alınır. Bu alternatif sorgular (uygun) endekslerden çok daha kolay bir şekilde faydalanabilir. Ayrıca şunu bilmemiz gerekir: kardinaliteler (her tabloda kaç satır var?), Tam tablo tanımı dahil. veri tipleri, kısıtlamalar, indeksler, ... ( \d tblpsql'de kullanın ), Postgres versiyonunuz ve min. / maks. mal başına fiyat sayısı.
Erwin Brandstetter

@ErwinBrandstetter Benden bir cevap kabul etmemi mi istiyorsun? Hangisinin en iyi olduğunu bilmeye gerçekten yetkin değilim, ancak sizinki en çok oyuna sahip olduğu için kabul etmekten mutluyum.
Tom Ellis

Yalnızca sorunuzu yanıtlarsa veya sizin için çalışıyorsa kabul edin. İlgili davalara yardımcı olabilirse nasıl devam ettiğinize dair bir yorum bile bırakabilirsiniz . Sorunuzun cevaplanmadığını düşünüyorsanız, bize bildirin.
Erwin Brandstetter

1
O zaman özür dilemeliyim, çünkü mükemmel cevaplar gibi görünen bir şey almış olsam da, artık soruyu kışkırtan problem üzerinde çalışmıyorum, bu yüzden hangisinin en iyi cevap olduğunu ya da herhangi birinin gerçekten hangisi olduğunu yargılayacak yerim yok Kullanım durumum için gerçekten uygun (olduğu gibi). Bazı DBA.Stackexchange ettiquette varsa, bu durumda izlemeliyim lütfen bana bildirin.
Tom Ellis 11

Yanıtlar:


42

Bu , şartlara ve kesin gereksinimlere bağlıdır . Bu soruya yorumumu düşünün .

Basit çözüm

DISTINCT ONPostgres ile birlikte :

SELECT DISTINCT ON (i.good, i.the_date)
       i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM   inventory  i
LEFT   JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER  BY i.good, i.the_date, p.the_date DESC;

Sipariş edilen sonuç.

Veya NOT EXISTSstandart SQL'de (bildiğim her RDBMS ile çalışıyor):

SELECT i.the_date, p.the_date AS pricing_date, i.good, i.quantity, p.price
FROM   inventory  i
LEFT   JOIN price p ON p.good = i.good AND p.the_date <= i.the_date
WHERE  NOT EXISTS (
   SELECT 1 FROM price p1
   WHERE  p1.good = p.good
   AND p1.the_date <= i.the_date
   AND p1.the_date >  p.the_date
   );

Aynı sonuç, ancak rasgele sıralama düzeni - eğer sürece ORDER BY.
Veri dağılımına, kesin gerekliliklere ve endekslere bağlı olarak, bunlardan biri daha hızlı olabilir.
Genel DISTINCT ONolarak, zafer kazanan kişidir ve bunun üzerine düzenli bir sonuç alırsınız. Ancak bazı durumlar için diğer sorgu teknikleri henüz çok daha hızlıdır. Aşağıya bakınız.

Maks / dak değerlerini hesaplamak için alt sorgular içeren çözümler genellikle daha yavaştır. CTE'li varyantlar genellikle daha yavaştır.

Sade görüşler (başka bir cevabın önerdiği gibi) Postgres'teki performansta hiç yardımcı olmuyor.

SQL Fiddle.


Uygun çözüm

Dizeler ve harmanlama

Her şeyden önce, alt-optimal bir masa düzeninden muzdaripsiniz. Önemsiz görünebilir, ancak şemanızı normalleştirmek çok uzun bir sürebilir.

Karakter türlerinetextvarchar göre sıralama ( ,, ...) yerel ayara göre yapılmalıdır - özellikle COLLATION . Büyük olasılıkla DB'niz bazı yerel kurallar kullanıyor (benim durumumda olduğu gibi:) de_AT.UTF-8. Şununla öğrenin:

SHOW lc_collate;

Bu sıralama ve dizin aramaları yavaşlatır . Dizeleriniz (mal isimleri) ne kadar uzun olursa o kadar kötü olur. Çıktınızdaki harmanlama kurallarını (veya sıralama düzenini) gerçekten önemsemiyorsanız, şunu eklerseniz daha hızlı olabilir COLLATE "C":

SELECT DISTINCT ON (i.good COLLATE "C", i.the_date)
       i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM   inventory  i
LEFT   JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER  BY i.good COLLATE "C", i.the_date, p.the_date DESC;

Harmanlamayı iki yerde nasıl eklediğime dikkat edin.
Testimde iki kat hızlı ve 20k satırlık her biri ve çok basit isimler ('iyi123').

indeks

Sorgunuzun bir dizin kullanması gerekiyorsa, karakter verileri içeren sütunların eşleşen bir harmanlama kullanması gerekir ( goodörnekte):

CREATE INDEX inventory_good_date_desc_collate_c_idx
ON price(good COLLATE "C", the_date DESC);

SO ile ilgili bu cevabın son iki bölümünü okuduğunuzdan emin olun:

Aynı sütunlarda farklı harmanlamalara sahip birden fazla dizin bile oluşturabilirsiniz - ayrıca diğer sorgularda başka bir (veya varsayılan) harmanlamaya göre sıralanan mallara ihtiyacınız varsa.

normalleştirmek

Yedekli dizeler de (iyi adı) tablolarınızı ve dizinlerinizi şişirir ve bu da her şeyi daha da yavaşlatır. Uygun bir masa düzeni ile başlamak için sorunun çoğunu önleyebilirsiniz. Bu gibi görünebilir:

CREATE TABLE good (
  good_id serial PRIMARY KEY
, good    text   NOT NULL
);

CREATE TABLE inventory (
  good_id  int  REFERENCES good (good_id)
, the_date date NOT NULL
, quantity int  NOT NULL
, PRIMARY KEY(good_id, the_date)
);

CREATE TABLE price (
  good_id  int     REFERENCES good (good_id)
, the_date date    NOT NULL
, price    numeric NOT NULL
, PRIMARY KEY(good_id, the_date));

Birincil anahtarlar otomatik olarak ihtiyacımız olan tüm indeksleri (neredeyse) sağlar.
Eksik ayrıntılar üzerinde bir bağlı çok sütun dizini üzerinde priceperformansını artırabilir ikinci sütun üzerinde azalan ile:

CREATE INDEX price_good_date_desc_idx ON price(good, the_date DESC);

Yine, harmanlama sorgunuzla eşleşmelidir (yukarıya bakın).

Postgres 9.2 veya sonraki sürümlerde, yalnızca indeksli taramalar için "örtü endeksleri" biraz yardımcı olabilir - özellikle tablolarınız ek sütunlara sahipse, tabloyu örtü indeksinden önemli ölçüde büyük yapar.

Bu ortaya çıkan sorgular çok daha hızlı:

VAR DEĞİL

SELECT i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM   inventory  i
JOIN   good       g USING (good_id)
LEFT   JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
AND    NOT EXISTS (
   SELECT 1 FROM price p1
   WHERE  p1.good_id = p.good_id
   AND    p1.the_date <= i.the_date
   AND    p1.the_date >  p.the_date
   );

DISTINCT ON

SELECT DISTINCT ON (i.the_date)
       i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM   inventory  i
JOIN   good       g USING (good_id)
LEFT   JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
ORDER  BY i.the_date, p.the_date DESC;

SQL Fiddle.


Daha hızlı çözümler

Bu hala yeterince hızlı değilse, daha hızlı çözümler olabilir.

Özyinelemeli CTE / JOIN LATERAL/ korelasyonlu alt sorgu

Özellikle mal başına birçok fiyat içeren veri dağıtımları için :

Materyalleştirilmiş görünüm

Bunu sık ve hızlı bir şekilde çalıştırmanız gerekirse, materyalize bir görünüm yaratmanızı öneririm. Geçmiş tarihler için fiyat ve stokların nadiren değiştiğini varsaymanın güvenli olduğunu düşünüyorum. Sonucu bir kez hesaplayın ve anlık görüntüsü materyalize görünüm olarak kaydedin.

Postgres 9.3+, materyalize görünümler için otomatik desteğe sahiptir. Eski sürümlerde basit bir sürümü kolayca uygulayabilirsiniz.


3
Önerdiğiniz price_good_date_desc_idxendeks, benzer bir sorgulama için performansı önemli ölçüde artırdı. Sorgu planım maliyetten 42374.01..42374.86aşağıya 0.00..37.12!
cimmanon

@cimmanon: Güzel! Çekirdek sorgu özelliğiniz nedir? VAR DEĞİL Mİ? DISTINCT ON? GRUP TARAFINDAN?
Erwin Brandstetter

DISTINCT ON
cimmanon

6

Bilginize, ben mssql 2008 kullandım, bu yüzden Postgres "include" dizini olmaz. Bununla birlikte, aşağıda gösterilen temel indeksleme kullanımı, karma birleşimlerden Postgres'teki birleştirme birleştirmelerine değişecektir: http://explain.depesz.com/s/eF6 (indeks yok) http://explain.depesz.com/s/j9x ( katılım kriterleri endeksi ile)

Sorgunuzu iki bölüme ayırmanızı öneriyorum. İlk olarak, envanter tarihleri ​​ile fiyatlandırma tarihleri ​​arasındaki ilişkiyi temsil eden çeşitli diğer bağlamlarda kullanılabilecek bir görünüm (performansı arttırmaya yönelik değildir) .

create view mostrecent_pricing_dates_per_good as
select i.good,i.date i_date,max(p.date)p_date
  from inventory i
  join price p on i.good = p.good and i.date >= p.date
 group by i.good,i.date;

Ardından, sorunuz varsa sorgulama (diğer fiyatlandırma tarihleri ​​olmayan envanteri bulmak için sol birleşimleri kullanmak gibi), diğer türler için manipüle etmek daha basit ve kolay olabilir:

select i.good
       ,i.date inventory_date
       ,i.quantity
       ,p.date pricing_date
       ,p.price       
  from inventory i
  join price p on i.good = p.good
  join mostrecent_pricing_dates_per_good x 
    on i.good = x.good 
   and p.date = x.p_date
   and i.date = x.i_date

Bu, aşağıdaki yürütme planını verir: http://sqlfiddle.com/#!3/24f23/1 indeksleme yok

... Tüm taramalı tüm taramalar. Karma eşleşmelerin performans maliyeti, toplam maliyetin büyük bir kısmını kaplar ... ve tablonun taranmasının ve sıralamasının yavaş olduğunu biliyoruz (hedefe kıyasla: dizin arar).

Şimdi, birleşiminizde kullanılan ölçütlere yardımcı olmak için temel dizinler ekleyin (bunların optimal dizinler olduğunu iddia etmiyorum, ancak noktayı gösterirler): http://sqlfiddle.com/#!3/5ec75/1 temel indeksleme ile

Bu gelişme gösterir. İç içe döngü (iç birleştirme) işlemleri artık sorgu için ilgili toplam maliyeti almaz. Maliyetin geri kalanı şimdi endeks arayışlarına yayılıyor (her stok satırını çektiğimiz için envanter taraması). Ancak sorgu daha iyi yapabiliriz çünkü sorgu miktar ve fiyatı alıyor. Bu verileri almak için, birleştirme ölçütünü değerlendirdikten sonra aramalar yapılmalıdır.

Son yineleme, planın kaymasını ve ek olarak istenen verileri doğrudan dizinin dışına çıkarmasını kolaylaştırmak için dizinlerde "içerir" i kullanır. Yani aramalar gitti: http://sqlfiddle.com/#!3/5f143/1 görüntü tanımını buraya girin

Şimdi, sorgunun toplam maliyetinin çok hızlı endeks arama işlemleri arasında eşit olarak dağıtıldığı bir sorgu planımız var. Bu, olabildiğince iyi olacak. Elbette diğer uzmanlar bunu daha da geliştirebilir, ancak çözüm birkaç önemli endişeyi ortadan kaldırıyor:

  1. Veritabanınızda, uygulamanın diğer alanlarında oluşturulması ve yeniden kullanımı daha kolay olan anlaşılır veri yapıları oluşturur.
  2. En pahalı sorgulama işleçlerinin tümü, bazı temel indeksleme kullanılarak sorgu planından çıkarıldı.

3
Bu iyi (SQL Server için) ancak benzerlikler olsa da farklı DBMS'leri optimize etmek için de ciddi farklılıklar var.
ypercubeᵀᴹ 09:13

@ypercube bu doğru. Postgres ile ilgili bazı özellikler ekledim. Niyetim, burada gösterilen düşünce sürecinin çoğunun, DBMS'ye özgü özelliklerden bağımsız olarak uygulanmasıydı.
cocogorilla

Cevap çok derin, bu yüzden denemek biraz zaman alacak. Nasıl devam ettiğimi bilmene izin vereceğim.
Tom Ellis,

5

PostgreSQL 9.3'e sahipseniz (bugün piyasaya sürülür) LATERAL JOIN kullanabilirsiniz.

Bunu sınamak için hiçbir yolum yok ve daha önce hiç kullanmamıştım, ancak belgelerden anlatabileceğimden sözdizimi şöyle bir şey olurdu:

SELECT  Inventory.Date,
        Inventory.Good,
        Inventory.Quantity,
        Price.Date,
        Price.Price
FROM    Inventory
        LATERAL
        (   SELECT  Date, Price
            FROM    Price
            WHERE   Price.Good = Inventory.Good
            AND     Price.Date <= Inventory.Date
            ORDER BY Price.Date DESC
            LIMIT 1
        ) p;

Bu temelde SQL Server'ın APPLY'sine eşdeğerdir ve bunun demo amaçlı SQL-Fiddle'da çalışan bir örneği vardır .


5

Erwin ve diğerlerinin belirttiği gibi, verimli bir sorgu birçok değişkene bağlıdır ve PostgreSQL bu değişkenlere dayanarak sorgu yürütmeyi optimize etmek için çok çaba harcar. Genel olarak, önce netlik için yazmak, ardından tıkanıklıkları belirledikten sonra performans için değişiklik yapmak istersiniz.

Ek olarak PostgreSQL, işleri biraz daha verimli hale getirmek için kullanabileceğiniz pek çok püf noktası içermektedir (biri için kısmi indeksler);

Denenmesi gereken ilk şey, sadece bir görünüm yapmak ve katılmak:

CREATE VIEW most_recent_rows AS
SELECT good, max(date) as max_date
FROM inventory
GROUP BY good;

Bu gibi bir şey yaparken iyi performans göstermelidir:

SELECT price 
  FROM inventory i
  JOIN goods g ON i.goods = g.description
  JOIN most_recent_rows r ON i.goods = r.goods
 WHERE g.id = 123;

O zaman buna katılabilirsin. Sorgu, görünümü temel tabloya karşı birleştirmeye başlayacak, ancak üzerinde benzersiz bir dizin bulunduğunu varsayarak (tarih, bu sırayla iyi ), iyi bir durumda olduğunuzu varsayalım (çünkü bu basit bir önbellek araması olacaktır). Bu, birkaç sıra bakıldığında çok iyi çalışacaktır ancak milyonlarca mal fiyatını sindirmeye çalışıyorsanız çok verimsiz olacaktır.

Yapabileceğiniz ikinci şey, envanter tablosuna en iyi bir bool sütunu eklemek ve

create unique index on inventory (good) where most_recent;

Daha sonra, bir mal için yeni bir satır eklendiğinde, en yanlış olanı yanlış olarak ayarlamak için tetikleyicileri kullanmak istersiniz. Bu, hatalar için daha fazla karmaşıklık ve daha fazla şans getirir, ancak faydalıdır.

Yine de bunun çoğu uygun indekslerin mevcut olmasına bağlıdır. En son tarih sorguları için, muhtemelen tarihte bir dizininiz olmalı ve tarih ile başlayan ve birleşme ölçütleriniz de dahil olmak üzere olası bir çok sütunlu bir sütun olmalıdır.

Güncellemek Erwin Erwin'in aşağıdaki yorumunda, bunu yanlış anladım gibi görünüyor. Soruyu tekrar okurken, ne sorulduğundan emin değilim. Güncellemede gördüğüm potansiyel sorunun ne olduğunu ve bunun neden belirsiz olduğunu belirtmek istiyorum.

Sunulan veritabanı tasarımının ERP ve muhasebe sistemleri ile gerçek bir IME kullanımı yoktur. Belirli bir ürünün belirli bir gününde satılan her şeyin aynı fiyata sahip olduğu varsayımsal mükemmel bir fiyatlandırma modelinde işe yarayacaktır. Ancak bu her zaman böyle değildir. Döviz bozdurma gibi şeyler için bile geçerli değil (bazı modellerde olduğu gibi). Bu, tartışmalı bir örnek ise, belirsizdir. Gerçek bir örnek ise, veri seviyesinde tasarımla ilgili daha büyük problemler var. Burada bunun gerçek bir örnek olduğunu varsayacağım.

Sen olamaz bu tarihten yalnız belirli bir iyiliği fiyatını belirler varsayalım. Herhangi bir işteki fiyatlar, karşı taraf başına ve hatta bazen işlem başına müzakere edilebilir. Bu nedenle size gerçekten gereken aslında ya da (Envanter tablosunda) dışarı envanter kolları tabloda fiyatı saklayın. Böyle bir durumda, tarih / mal / fiyat tablonuz yalnızca pazarlığa bağlı olarak değişebilecek baz bir fiyat belirler. Böyle bir durumda bu problem, bir raporlama probleminden, işlem yapan ve her seferinde bir tablodaki bir satırda çalışan probleme gider. Örneğin, belirli bir günde belirli bir ürünün varsayılan fiyatını şu şekilde arayabilirsiniz:

 SELECT price 
   FROM prices p
   JOIN goods g ON p.good = g.good
  WHERE g.id = 123 AND p."date" >= '2013-03-01'
  ORDER BY p."date" ASC LIMIT 1;

Fiyatlar endeksi ile (iyi, tarih) bu iyi sonuç verecektir.

Ben bu tartışmalı bir örnek, belki de üzerinde çalıştığın şeye daha yakın bir şey yardımcı olabilir.


most_recentYaklaşım En son fiyat için iyi çalışması gerekir kesinlikle . Öyle görünüyor ki OP her envanter tarihine göre en yeni fiyata ihtiyaç duyuyor .
Erwin Brandstetter

İyi bir nokta. Yeniden okuma, önerilen verilerle bazı gerçek pratik eksiklikleri tespit etsem de, bunun yalnızca tartışmalı bir örnek olup olmadığını söyleyemem. Kararlı bir örnek olarak, neyin eksik olduğunu söyleyemem. Belki de bunu işaret edecek bir güncelleme de olabilirdi.
Chris Travers

@ChrisTravers: Kararlı bir örnek, ancak birlikte çalıştığım şemayı gönderme konusunda özgür değilim. Belki de hangi pratik eksiklikleri tespit ettiğinizle ilgili bir miktar söyleyebilirsiniz.
Tom Ellis,

Kesin olması gerektiğini düşünmüyorum, ama alegoride kaybedilen sorun hakkında endişeli. Biraz daha yakın bir şey yardımcı olabilir. Mesele şu ki, fiyatlandırmada, belirli bir gündeki fiyatın varsayılan olması muhtemeldir ve sonuç olarak bu işlemi yalnızca işlem girişi için varsayılan olarak rapor etmek için kullanmazsınız, bu nedenle ilginç sorgularınız tipik olarak yalnızca birkaç satırdadır. saati.
Chris Travers

3

Başka bir yol lead(), tablo fiyatındaki her satır için tarih aralığını almak için pencere işlevini kullanmak ve ardından betweenenvantere katılırken kullanmaktır . Aslında bunu gerçek hayatta kullandım, ama bunun nedeni, bunun nasıl çözüleceği konusundaki ilk fikrimdi.

with cte as (
  select
    good,
    price,
    date,
    coalesce(lead(date) over(partition by good order by date) - 1
            ,Now()::date) as ndate
  from
    price
)

select * from inventory i join cte on
  (i.good = cte.good and i.date between cte.date and cte.ndate)

SqlFiddle


1

Fiyat sekmesinden gelen kayıtları yalnızca envanter tarihinde veya daha önce olanlarla sınırlayan birleştirme koşullarını içeren bir envanter fiyatını kullanın, ardından en fazla tarihi ve tarihin o alt kümeden en yüksek tarihi alın

Yani envanter fiyatınız için:

 Select i.date, p.Date pricingDate,
    i.good, quantity, price        
 from inventory I join price p 
    on p.good = i.good
        And p.Date = 
           (Select Max(Date from price
            where good = i.good
               and date <= i.Date)

Belirtilen herhangi bir malın fiyatı aynı günde bir kereden fazla değiştiyse ve bu sütunlarda gerçekten yalnızca tarihler ve saatler bulunmuyorsa, fiyat değişikliği kayıtlarından yalnızca birini seçmek için katılımlara daha fazla kısıtlama uygulamanız gerekebilir.


Maalesef işleri hızlandırıyor gibi görünmüyor.
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.