Numerowanie listy zwracanych rekordów w MySQL

Przy różnorakich konkursach często zachodzi potrzeba wyciągnięcia pozycji konkretnego użytkownika (wpisu w bazie) na tle innych użytkowników (wpisów) w ograniczeniu czasowym.

Czyli np. mamy 10 tys wyników gry z danego dnia i chcemy poznać pozycje w rankingu, które zajmują użytkownicy o identyfikatorach 1, 2, 3.

Oczywistym rozwiązaniem jest zwykły selekt z ORDER BY po polu z punktami i następnie iterowanie w php, jednak gdy liczba danych będzie większa taki SELECT nie wykona się lub będzie bardzo niewydajny.

Rozwiązaniem dużo lepszym jest użycie podzapytania i wykonania obliczeń numerowania na samej bazie a następnie zwrócenie do php jedynie rekordów pasujących do naszych użytkowników. Czytaj dalej Numerowanie listy zwracanych rekordów w MySQL

Relecyjna baza ‘ala MySQL’ w chmurze Amazon AWS

Amazon w ramach AWS (Amazon Web Services) uruchomił publiczną wersję beta usługi relacyjnaj bazy danych ‚ala MySQL’ (Amazon RDS) w swojej chmurze.

Amazon RDS ma cechować się:

  • prostotą konfiguracji i wdrożenia do aplikacji,
  • pełną kompatybilnością z bazą danych MySQL,
  • prostotą w zarządzaniu bazami, backupami i dostępnymi narzędziami,
  • pełną automatyką w backup’owaniu i czynnościach serwisowych,
  • skalowalnością w zależności od potrzeb uzytkownika
  • niezawodnością
  • bardzo dostępną ceną, tak jak w przypadku innych usług developerzy kasowani będą jedynie za zaosby, które rzeczywiście zużywają ich aplikacje.

Czy rzeczywiście tak będzie? Mam nadzieje. Przeczytaj więcej o Amazon RDS.

Usunięcie nieużywanych tagów z bazy WordPress’a

Czasami zachodzi potrzeba „ręcznego” kasowania postów z WordPress’a, jako że system ten działa na MySQL MyISAM to nie wspiera kaskadowego kasowania zależnych danych.

Skasowanie postów jest stosunkowo proste:

DELETE FROM wp_posts WHERE conditions;

Trudniejsza sprawa jest ze skasowaniem tagów. Na necie znalazłem poniższe zapytanie, działa wyśmielicie:

DELETE a,b,c
FROM
	wp_terms AS a
	LEFT JOIN wp_term_taxonomy AS c ON a.term_id = c.term_id
	LEFT JOIN wp_term_relationships AS b ON b.term_taxonomy_id = c.term_taxonomy_id
WHERE (
	c.taxonomy = 'post_tag' AND
	c.count = 0
	);

Czasami jednak pole „count” w relacji „wp_term_taxonomy” zawiera niepoprawne dane (liczbę większą od 0), wtedy nieużywane tagi musimy zidentyfikować poprzez LEFT JOINa z warunkiem ID IS NULL. Poniżej zapytanie wyświetlające nieużywane tagi.

SELECT DISTINCT wp_terms.slug FROM wp_terms wt
	INNER JOIN wp_term_taxonomy wtt ON wt.term_id=wtt.term_id
	INNER JOIN wp_term_relationships wtr ON wtr.term_taxonomy_id=wtt.term_taxonomy_id
	LEFT JOIN wp_posts wp ON wp.ID=wtr.object_id
WHERE
	taxonomy='post_tag'
AND 
	ID IS NULL
AND 
	NOT EXISTS(SELECT * FROM wp_terms wt2
                INNER JOIN wp_term_taxonomy wtt2 ON wt2.term_id=wtt2.term_id WHERE wtt2.parent=wt.term_id)
ORDER BY name

Zrzut danych z bazy do pliku w MySQL

Metoda I

Zapytanie z poziomu bazy danych.

mysql> SELECT *
    -> INTO OUTFILE 'c:/data.csv'
    -> FIELDS TERMINATED BY ','
    -> ENCLOSED BY '"'
    -> ESCAPED BY '\\'
    -> LINES TERMINATED BY '\r\n'
    -> FROM table_to_export;
Query OK, 20 rows affected (0.02 sec)

Metoda II

Zapytanie z poziomu konsoli bez logowania się do bazy danych.

mysql -u user -h host -p --quick -e 'SELECT * FROM table_to_export' baza > data.txt

Warto pamiętać, że w metodzie I plik zapisywany jest po stronie serwera mySQL a w metodzie II po stronie klienta mySQL. Zatem gdy łączymy się z bazą danych z innego serwera niż wykonujemy zapytanie to należy użyć metody II.

Aktualny czas a transakcja w PostgreSQL

Ostatnio miałem ciekawy case’ik – musiałem w obrębie jednej transakcji zmienić dane rekordów. Wszystko odbywało się w kilku funkcjach plpgsql, funkcje mogły się wywoływać rekurencyjnie w triggerach a zmiany dotyczyły między innymi pól typu TIMESTAMP. Wartości pól TIMESTAMP rekordów były zmieniane i ich zmiany automatycznie wpływały na w działanie skryptu.

Jakież było moje zdziwienie gdy skrypt nie chciał działać, a wszystkie rekordy miały taką zamą wartość pola typu TIMESTAMP. Otóż okazało się, że po rozpoczęciu transakcji funkcje zwracające aktualny czas:

SELECT CURRENT_TIME;
SELECT CURRENT_TIMESTAMP;
SELECT NOW();

przestały zwracać aktualny czas a zamiast tego czas rozpoczęcia transakcji.

I o dziwo jest to rzeczywiście poprawne działanie. Na stronie http://www.postgresql.org/docs/8.3/static/functions-datetime.html można przeczytać:

Since these functions return the start time of the current transaction, their values do not change during the transaction. This is considered a feature: the intent is to allow a single transaction to have a consistent notion of the „current” time, so that multiple modifications within the same transaction bear the same time stamp.

Zatem wyjściem z tego problemu jest użycie postgres’owej funkcji timeofday() i zrzutowanie jej wyniku na TIMESTAMP, realizyje to zapytanie:

SELECT cast( timeofday() AS timestamp);

Edit: zamiast timeofday() można uzyć funkcji clock_timestamp(), która zwraca date w postaci TIMESTAMP’a

SELECT clock_timestamp();

Dla ciekawości sprawdziłem jak zachowują się inne dialekty SQL i okazało się, że zarówno MySQL jak i MSSQL realizują tą sytuację odmniennie niż PostgreSQL, zatem:

  • MySQL – w transakcji zwracany jest bieżący czas (nie czas rozpoczęcia transakcji)
  • MSSQL – w transakcji zwracany jest bieżący czas (nie czas rozpoczęcia transakcji)
  • PgSQL – w transakcji zwracany jest czas rozpoczęcia transakcji

Jak usunąć zduplikowane rekody w tabeli bazy danych?

Czasami zdarza się że, poprzez nie do końca poprawną walidację danych wejściowych lub z powodu błędnego działania aplikacji stworzą nam się w bazie danych zduplikowane rekordy.

Takie niepożądane zduplikowane dane w 90% będą nam przeszkadzać i w najlepszym razie mogą powodować niepotrzebny zamęt w aplikacji. Skutki mogą takżę spowodować dużo poważniejsze problemy jak chociażby rozsynchronizowanie się danych w powiązanych tabelach w bazie.

Co należy robić w przypadku wykrycia zduplikowanych rekordów?

W pierwszej kolejności trzeba dojść do tego w jaki sposób powstały a następnie trzeba usprawnić aplikację w taki sposób aby powstawanie duplikatów było już niemożliwe.
Następnie trzeba wziąć się za zapisane dane i należy usunąć z nich nadmiarowe rekordy.

Usuwanie zduplikowanych rekordów

Usuwać duplikaty można na conajmniej kilka sposobów. Najczęstszą radą spotykaną w internecie to przeniesienie unikalnych rekordów do nowej tymczasowej tabeli, skasowanie wszystkich danych ze starej tabeli, a następnie zgranie z powrotem rekordy unikalne.

Niestety nie jest to metoda ani szybka, ani skuteczna ani bezpieczna.

Na prościutkim przykładzie rzedstawię poniżej najlepszy sposób na pozbycie się duplikatów

Tworzymy przykładową tabelę:

CREATE TABLE people(
	id SERIAL PRIMARY KEY,
	name VARCHAR(100) NOT NULL,
	surname VARCHAR(100) NOT NULL,
	phone VARCHAR(100) NOT NULL,
	email VARCHAR(100) NOT NULL
);

Tabela zawiera autoinkrementowane pole id, które jest kluczem jej głównym i kilka pól przetrzymujących dane.

Wstawiamy zduplikowane rekordy:

INSERT INTO people(name,surname,phone,email) VALUES('Marcin','Szajda','660111111','marcin@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Marcin','Szajda','660111111','marcin@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Agnieszka','Czopek','550123123','aga@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Jas','Kowalski','555123147','jas@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Jas','Kowalski','555123147','jas@nospam.pl');

Wyświetlamy zawartość tabeli:

SELECT * FROM people;
 id |   name    | surname  |   phone   |      email       
----+-----------+----------+-----------+------------------
  1 | Marcin    | Szajda   | 660111111 | marcin@nospam.pl
  2 | Marcin    | Szajda   | 660111111 | marcin@nospam.pl
  3 | Agnieszka | Czopek   | 550123123 | aga@nospam.pl
  4 | Jas       | Kowalski | 555123147 | jas@nospam.pl
  5 | Jas       | Kowalski | 555123147 | jas@nospam.pl
(5 rows)

Jak widzimy mamy zduplikowane 2 rekordy, pierwszym krokiem do usunięcia rekordów będzie napisanie zapytania, które je wyświetli a następnie
zmienimy SELECT NA DELETE.

SELECT
	id, name, surname, phone, email
FROM
	people
WHERE
	EXISTS (
		SELECT
			NULL
		FROM
			people AS P
		WHERE
			people.name = P.name
			AND people.surname = P.surname
			AND people.phone = P.phone
			AND people.email = P.email
		GROUP BY
			P.name, P.surname, P.phone, P.email
		HAVING
			people.id < MAX(P.id)
	);

Wynikiem zapytania będzie:

 id |  name  | surname  |   phone   |      email       
----+--------+----------+-----------+------------------
  1 | Marcin | Szajda   | 660111111 | marcin@nospam.pl
  4 | Jas    | Kowalski | 555123147 | jas@nospam.pl
(2 rows)

Wyjaśnienie zapytania:

  • SELECT _FIELDS_ FROM people - deklarujemy pola, które checmy wyświetlić
  • WHERE EXISTS( _CONDITION_ ) - które spełniają warunek _CONDITION_
  • SELECT NULL FROM people AS P - w podzapytaniu nic nie wyświetlamy, wykorzystujemy je jedynie do złączenia
  • WHERE _CONDITION_ - joinujemy po wszystkich polach, które mają tworzyć unikalny klucz
  • GROUP BY _FIELDS_ - grupujemy wg. unikalnych pól
  • HAVING people.id < MAX(P.id) - warunek na id

Modyfikujemy teraz powyższe zapytanie SELECT _FIELDS_ na DELETE

DELETE
FROM
	people
WHERE
	EXISTS (
		SELECT
			NULL
		FROM
			people AS P
		WHERE
			people.name = P.name
			AND people.surname = P.surname
			AND people.phone = P.phone
			AND people.email = P.email
		GROUP BY
			P.name, P.surname, P.phone, P.email
		HAVING
			people.id < MAX(P.id)
	);

I otrzymujemy tabele wyczyszczoną z nadmiarowych rekordów.

Jakie to proste.

**********************edit**********************

Jak słusznie zauważył kolega Wild Child powyższe zapytanie nie działa w mySQL.
Przyznaje się bez bicia, że testowałem jedynie w PostgreSQL gdyż powyższy kod SQL
wyglądał na uniwersalny.

Okazało się jednak, że mySQL nie pozwala na jednoczesne wyświetlanie i modyfikowanie
zawartości tabeli. (jednoczesny DELETE w zapytaniu głównym i SELECT w podzapytaniu)

Posiedziałem trochę i wymyśliłem alternatywne zapytanie:

SELECT 
	D.*
FROM 
	people as U
INNER JOIN 
	people AS D ON (
	   	D.name = U.name AND
   		D.surname = U.surname AND
	   	D.phone = U.phone AND
	   	D.email = U.email
	   )
AND 
	D.id > U.id;

Konstrukcja tego zapytania jest nieco inna:

  • Robimy JOINA wewnątrz tabeli po polach, które mają być unikalne
  • Zostawiamy rekord o najniższym id, a resztę kasujemy (warunek AND D.id > U.id)
  • Skróty: D - duplicate, U - unique
  • Oczywiście aby usunąć rekordy należy zamienić słowo SELECT na DELETE