Making Data Dance
This is a modal window.
The media could not be loaded, either because the server or network failed or because the format is not supported.
Formal Metadata
Title |
| |
Title of Series | ||
Part Number | 71 | |
Number of Parts | 94 | |
Author | ||
License | CC Attribution - ShareAlike 3.0 Unported: You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal and non-commercial purpose as long as the work is attributed to the author in the manner specified by the author or licensor and the work or content is shared also in adapted form only under the conditions of this | |
Identifiers | 10.5446/30681 (DOI) | |
Publisher | ||
Release Date | ||
Language |
Content Metadata
Subject Area | ||
Genre | ||
Abstract |
|
RailsConf 201571 / 94
1
4
7
8
9
10
11
13
14
16
17
19
21
24
25
29
30
33
34
35
36
37
39
40
42
47
48
49
50
51
53
54
55
58
59
61
62
64
65
66
67
68
70
71
77
79
81
82
85
86
88
92
94
00:00
Analytic setServer (computing)BlogDatabaseQuery languageMultilaterationData conversionFront and back endsRow (database)WordMultiplication signFunctional (mathematics)AreaRight angleInternational Date LineBitSpacetimeOcean currentPattern languageInheritance (object-oriented programming)Group actionGrass (card game)Beta functionMappingPortable communications deviceSequelRun time (program lifecycle phase)Revision controlProduct (business)Series (mathematics)QuicksortGeometryLattice (order)Raw image formatComputer animation
05:16
Projective planeDatabaseSoftware testingConnected spaceRow (database)Point (geometry)Set (mathematics)CalculationQuery languageMultiplication signData typeConcurrency (computer science)Protein foldingSingle-precision floating-point formatPortable communications deviceMereologyDifferent (Kate Ryan album)WindowInterface (computing)Window functionTable (information)CASE <Informatik>Closed setOpen setLocal ringProduct (business)Sampling (statistics)NumberFigurate numberRecurrence relationPartition (number theory)Charge carrierRight angleFunctional (mathematics)Revision controlScripting languageCore dumpCopyright infringementComputer animation
09:48
Partition (number theory)ProteinGroup actionDatabaseNumberRow (database)Mobile appMixed realityOrder (biology)Window functionDifferent (Kate Ryan album)Message passingField (computer science)Position operatorControl flowFunctional (mathematics)CalculationNeuroinformatikCountingWindowClient (computing)Right angleLoop (music)Table (information)Intrusion detection systemComputer animation
13:04
Row (database)Object (grammar)Connected spaceComputer fileSocial classQuery languageField (computer science)Raw image formatComputer animation
13:36
Direction (geometry)DatabaseRow (database)Data conversionEndliche ModelltheorieArray data structureField (computer science)Message passingObject (grammar)Computer animation
14:14
SubgroupFerry CorstenEvent horizonGroup actionQuery languageSelf-organizationMathematicsAreaBuildingRow (database)Flow separationLibrary (computing)Uniform resource locatorReading (process)Endliche ModelltheorieLogicObject (grammar)Software frameworkSystem callField (computer science)Order (biology)DatabaseRight angleLoginWater vaporClassical physicsTimestampComputer animation
17:08
Raw image formatPower (physics)DiagramCore dumpRight angleConnected spaceCode refactoringBuildingQuery languageComputer animation
17:37
Query languagePower (physics)CASE <Informatik>Latent heatExpressionTable (information)Time seriesVariable (mathematics)Computer animation
18:31
Level (video gaming)Inheritance (object-oriented programming)Table (information)DatabaseProcedural programmingQuery languageDifferent (Kate Ryan album)Traffic reportingGoodness of fitComputer configurationData storage deviceView (database)XMLComputer animation
19:32
View (database)ExistenceError messageFunction (mathematics)Row (database)Reverse engineeringDrop (liquid)Human migrationConnected spaceVideo gameSpacetimeRight angleWordComputer animation
20:25
CodePoint (geometry)DatabaseLogicInheritance (object-oriented programming)Right angleView (database)Query languageComputer animation
21:04
View (database)Query languageDrop (liquid)Subject indexingHuman migrationTraffic reportingResultantDatabaseMultiplication signReal-time operating systemCASE <Informatik>Table (information)Computer animation
22:33
DatabaseTable (information)View (database)Computer animation
23:11
Information securityReal numberMobile appQuery languageStructural loadProduct (business)Task (computing)DatabasePlug-in (computing)Order (biology)Traffic reportingBit rateScheduling (computing)BitPhysical lawSoftware testingWeb pageComputer animation
24:32
Row (database)Query languageView (database)Selectivity (electronic)Thresholding (image processing)Window functionFerry CorstenReading (process)Field (computer science)Integrated development environmentDemosceneSubgroupComputer animation
25:25
Binary codeField (computer science)Range (statistics)Multiplication signData typeFreewareDifferent (Kate Ryan album)Flow separationGraphical user interfaceFormal languageRevision controlKey (cryptography)Subject indexingData storage deviceGoodness of fitOcean currentInstallation artMobile appInformationWebsiteElectronic program guideValidity (statistics)Row (database)Query languageType theoryDatabaseTimestampHierarchyHydraulic jumpGeometryProcess (computing)Interface (computing)Repository (publishing)Special unitary groupPrice indexFinitismusComputing platformSharewareGraph coloringKeyboard shortcutPoint (geometry)Computer-assisted translationComputer animation
29:26
View (database)ExpressionTable (information)Order (biology)WritingTouchscreenDatabaseComputer-assisted translationComputer animation
Transcript: English(auto-generated)
00:15
So welcome to the data and analytics track here at RailsConf.
00:21
I'm Barrett Clark. I'm Barrett Clark, and we're going to talk about making data dance, or maybe you could think of this as hardest heretical anti-patterns for fun and profit.
00:40
I'm excited to see so many people here in the audience also want to play with data, are excited about data. I'm super excited about data, and I promise, this is a bold claim, but I promise there'll be at least one nugget in this whole mess that you'll be able to take and use. A little bit about me.
01:06
This is basically my resume where I've been for the last 15 or so years. I started out my Well, I started longer ago, but I spent a long time at AOL in a market research group,
01:20
and in production we used Berkeley DB. Yes, really, Berkeley DB. Informix, Sybase, two versions of Sybase actually, MySQL, Postgres, and SQL Server, and we did most of those the last four we did with Rails,
01:43
which was super exciting. Then I went to Geoforce, which is an asset tracking company that mostly works in the oil and gas space. So lots of geo data, really interesting stuff. So we used Postgres, PostGIS, and we used Redis there.
02:03
Right now I'm at Saber Labs. I've been there for two and a half years. We're an emerging tech incubator within Saber, which is a big company that I'm sure most of you actually have not heard of. If you know the brand Travelocity, we just sold that so I actually don't have anything to do with that anymore.
02:21
We use Postgres when I need to do GIS stuff, PostGIS usually, Redis, and occasionally MySQL. What do all these things have in common? What's so great about me, right? I've seen a lot of data, like I've seen a lot of really interesting data and a lot of it,
02:41
and I've generally played with it with Ruby. Not always, but generally. Then again, generally, I've played with it with Rails. Again, not always. You can do some really useful stuff with plain old Ruby. This talk is kind of based on a series of blog posts that I did a while back.
03:03
Postgres Weekly, the weekly newsletter, picked up a handful of them. I'm super excited about data, y'all. So, ActiveRecord. It's great. Handles most of your data access needs. It's a great place to start.
03:22
But I maintain that you're going to eventually outgrow it. And maybe even outgrow Rails, at least in the monorail or what do we call it now? Integrated, I don't know. But that's a conversation for later. We're not going to talk about that. Having said that, Rails is still our go-to tool in the lab,
03:44
especially when we need to build a backend. This isn't a talk about ActiveRecord, though. This is a talk about raw SQL. Now, where I go to raw SQL is kind of in these two areas.
04:02
You can build up complicated joins. You can do joins in ActiveRecord with scopes. You can chain scopes together, and that's great. But it also starts to get kind of ugly, right? So I find myself, rather than figuring out how all these things fit together, and then you've got this thing from this scope that's referenced in this other scope, and that's weird,
04:23
I'd rather just write the query. And then you've got deeper database functionality. ActiveRecord maps all of your CRUD functionality, and that's great. But if I want to use something in the database that's a little more robust or a little more complex or unique to that database,
04:43
I'm going to dig into the query, into the SQL. And also, really, if I'm going to do PostGIS, the Rgeogym is fantastic, but I know PostGIS. I know those functions, and so I don't really want to learn a new API
05:01
for this other thing that I already know. I use it some, but I also write the queries. So I know you're thinking, Barrett, dude, what about database portability, right? Everybody here is all freaked out, right? Okay, raise your hand if you've ever
05:20
changed databases in an established project. Okay, fantastic. How about in the last year? Have you done it? Okay, so some of you know it just doesn't work like that. Database portability is a pipe dream, and maybe y'all had a different experience than I have. I've done it a handful of times, and it really sucks.
05:45
Even beyond just the basic query interface that ActiveRecord gives you, like the data types are different. These are different databases. There are different data types, and even if you have like that really cool MySQL and a Postgres script, you still have to do a lot of work to migrate that data.
06:02
Takes a lot of effort. And then, back at AOL, we switched versions of Sybase, so we were within the same database, right? And even that was a lot of work. We did all the prep, migrated the data, tested everything, and it was great. We still spent a month fighting fires,
06:22
in part because of the DBA, but that's a different story. We still spent a month fighting fires. Database portability just doesn't work like that. So I say, you chose the whatever, the Postgres database for a reason. Let's use it. And now, it may not have been you who chose the database,
06:42
but let's just assume that you or that other person are not a total moron. I'm comfortable with that assumption today on this stage. Okay, some caveats. The more work you put on the database, the more that can become a single point of failure.
07:02
So pay attention to your connections. Pay attention to your connection pool, concurrent connections, and if you have a query that takes longer than 500 milliseconds, like you're doing it wrong. Something is bad. And probably you don't want it to spend even that much time in the database. A half a second is an insane amount of
07:21
time in the database. And we're going to talk about how to do that. And also point out, you need to be benchmarking your queries against production data because they're going to work a lot differently in your local dev database with a couple hundred records than they do in your production database
07:40
with a couple of million records. Cool. So let's get our hands dirty. Window functions. This is going to be the foundation of what we're going to talk about today. Has anybody here played with window functions in Postgres or Oracle, whatever? Okay, great. So a few of you know, this is way cool stuff.
08:02
So the documentation says a window function performs a calculation across a set of table rows that are somehow related to the current row. So we're going to do a calculation across some rows related to the current row. As far as documentation goes, the Postgres documentation, I think, is pretty good.
08:24
Calculation across a set of rows related to the current row. It's not bad. I think I can simplify it. You're going to fold data from other rows into this row. Okay, so there are probably a dozen, maybe more, window functions. These are the five, these window functions are my jam.
08:47
Lead and lag, first value, last value, and row number. So let's take a look. Given some cheesy or fruity sample data here, we're going to play with these functions.
09:01
So lead and lag are up first. We're going to take the lead ID and the lag ID from fruits. Note that over, open paren, close paren. That's where you define the window. We're not doing anything special in this particular case. We're just saying that the table is the window. So we're going to take the next ID and the previous ID for that given row.
09:23
So you see for the first row, the next ID is two. There is no previous ID, so it's null. And then on and on, the last row, there is no next ID, so it's null. Previous ID is nine. So we're doing calculation across some set of rows that are related to that current row.
09:46
So let's talk about how to define that. We need to partition our fruit. So here we're going to partition by the value of fruit and we're going to order it by ID.
10:01
So all occurrences of the similar fruits are going to be bunched together. We're going to chunk the table up by fruits. So that first value of the ID across the fruits, so for Apple we have four. The first ID is one. There's one banana and he's hanging in there at ten.
10:22
And then we've got five pairs. The first pair happens at four. If we were to look at the last value, Apple would be a five, banana still a ten, and pair is a nine. You see those IDs bounce around. That doesn't matter. We've rearranged the data by fruits.
10:44
We can round out my list of top five with row number. So we see each row's position within its window. And you notice each window function gets its own partition defined.
11:00
They can be different if you wanted to do that. They're not related at all. So for each row, it's computed across the rows that are within that partition. So Apple, one, two, three, four, one banana, five pairs, one, two, five. Great. Now let's look at a practical example.
11:22
We're going to make a chat app. So here you see we have some messages. We've got a couple of different rooms. And we want to make this app behave kind of like your message app on your phone. We're not going to call out the person's name with each message. So how do we do that?
11:40
So we could pull down all the data and we could loop through it and do all the stuff in the client. But that's a lot of work. So instead we're going to just let the database tell us what's the next ID. We're going to break the data up by room. Because we've got two rooms or n number of rooms. So for each room, and then we're going to order it by room ID, which isn't strictly necessary.
12:02
But when I'm looking at the data, it just makes it easier to look at if it's ordered. And then we're going to order it by message ID. So this allows us to hide that name banner for the second and third message because we see it's the same user ID. So we've got some user, the user, who's about to go to the airport.
12:24
And then they just landed. And wow. And it's some kind of a mess. Everybody else comes into that room and says, hey. So the second and third message in there doesn't have the person's name. And then meanwhile in some other room, somebody's waiting for an Uber. And then as a bonus pro tip, this doesn't have anything to do with window functions.
12:45
But you can use positional field numbers for your order by and your group by. And this is especially handy if you have a calculated field like a window function or a count or something. So you don't have to respecify that calculation.
13:04
Great. So this is RailsConf. How do we do this in Rails? This is how you would execute that raw SQL in ActiveRecord. You could also do find by SQL. But I just prefer ActiveRecord, base, connection, execute.
13:23
So we've got a calculated field in this query. That's not going to show up when you inspect the object because the class doesn't know about it. But it's there. Now, some of you may be thinking, well, this is a little weird. That's too, I'm uncomfortable with how anti-pattern that is.
13:42
I've had that conversation before. That's fine. You don't have to use ActiveRecord. You can just use the PG gem directly. Or if you were just using plain old Ruby, you know, you can talk to a database with plain old Ruby. That is the thing. And it might look something like this. So whereas, with ActiveRecord, you got an array
14:01
of message objects, just directly with the PG gem, you just get an array of field values. So you get an array of arrays with the data. As a reminder, models don't have to be backed by ActiveRecord. You don't have to inherit from ActiveRecord.
14:21
You don't even have to persist the data in the database. We can just put business logic in a class and call it a model. That's totally prominent. Great. So now we can answer questions like, when did something change? Like, when did somebody leave?
14:41
How long did each thing last? How long were people in which places? We can do enter and exit events. And you could totally pull all of that data down, all that transactional data, and you can munge it in your client, but that's a lot of objects, a lot of ActiveRecord objects, or a lot of stuff to send across the wire to your JavaScript framework.
15:04
But to do this, to answer these questions, we need more sophisticated queries. So we're going to use subqueries. You can subquery to filter and group. So you can wrap a query with an outer query to filter it and group it.
15:21
You can subquery for a field value. That's really cool. We're going to talk about that. And you can also do a subquery and a join. We're not going to talk about that, but it is a thing, and you can do it. So let's start with our base query. For a given phone, pull in the next minor field.
15:42
We're looking at data that comes in from a Bluetooth beacon, an iBeacon. So we're doing indoor location stuff here. And this is going to help us see how a phone moves around in an area covered by beacons. Again, that order by phone, there isn't strictly necessary,
16:01
but it just makes it easier to see. So we're going to order, we're going to partition the data by phone, so we have, for each of the phones, we'll have all of their readings, and they'll be in order by the reading timestamp. And then we're going to select some data out. We're trying to answer the question of when did something change.
16:21
So we're going to wrap that query in another query, and then we can say when that minor and the next minor were different, so now we have our change events. And we name our subqueries, so it's all that as beacon readings lead.
16:43
Yo, dog, I heard you like queries, so put a query in your query so you can query while you query. So now we can see how people move around in the building. Where did they move from and to, and how often did those things occur?
17:01
And the data looks something like this. I'll give you a second to digest that. Great. Really, this is what the data looks like. So I used D3 to make a core diagram to show the connections and show the intensity of those connections.
17:20
So this is the power of raw SQL. Don't be afraid of it. Okay. Let's refactor. When you're building up queries and nesting them and nesting them and nesting them, it can get unwieldy, so we have an answer to that. Common table expression.
17:42
So here was our original query with the outer query. We rewrite this with, it's also known as a with query because we have with, we're still naming our subquery as all that stuff, and then you see the bottom half, we just select from that, and it essentially becomes like a named variable for the query.
18:03
Now this case isn't really all that spectacular, right? That's not really that different. The power of common table expression comes in something like this, where you can build up bite-sized pieces of the query. The specifics of this query really aren't that important. It does some cool stuff.
18:21
I like it a lot. What we're doing here and all that stuff is we're just building up time series data, and we're putting it in hourly buckets. We're doing that so we can look at this heat map, and we can see how long were people in which places. Again, just using D3.
18:42
Now you may have noticed, hey, wait a minute, so we've got this apparent reporting app, and we're using this base query all over the place, what's going on? We shouldn't be repeating ourselves. Yeah, sometimes you may have a query that gets used a lot in a lot of different places, and that's cool. We can fix that problem. We can clean that up, and we're going
19:01
to do what's called a view, a stored query in the database. This is not a stored procedure. It's just a query that's saved in the database, and it looks like a read-only table. So to create it, we do create or replace. That or replace is optional, but it's a super good idea,
19:22
and it's going to save you from if you were to rerun it, and then it won't complain. It'll just overwrite it. So we've got this, and we can totally put this in an active record migration, which is a good idea. If you're doing this in a Rails app, you want to do this.
19:40
It's not a reversible migration, obviously, so we have to define the up and the down. And that execute is hanging out bare there, because within an active record migration, it understands that this is active record, base, connection, execute. And we're going to just pipe a here doc into that.
20:00
And that strip here doc is a cool trick, so when you're looking at the log output, it's not hanging out like way over on the right. It's going to strip all the leading spaces, and so it'll look reasonable in your log output. And then we drop the view with drop view if exists. Again, the if exists is optional, but it'll save you
20:21
from having an error if you did something dumb. So now we have this. We're going to just select from that view, and it's much cleaner, much easier to look at, and we can use that all over the place. And now you're saying, Barrett, we've got business logic in our database, and that's a super bad idea, right? Yeah, I know. I've had that religious battle, too.
20:42
I fought that religious battle, too. I'm not saying put all of your business logic in the database. I'm not saying start there. I'm just saying think about it. Talk it over with your coworkers or your peers. The point is that you can decide where your code runs and why.
21:05
Let's go faster. My query is too slow. Like, it's taking 500 or 600 milliseconds, and unicorns are crying. So let's do the heavy lifting in a separate process. And we can do that with a materialized view.
21:23
So whereas before we had a view where the query was stored in the database, when you execute that, it's going and fetching the results for you in real time. With a materialized view, we're going to take the definition of the query and materialize the results from it. We're going to store the results from the query ahead of time.
21:41
And it can then be indexed, which is great. So we're going to store the query definition and the data in the database. So we have create materialized view. We name it. And I would recommend, this may not be the canonical Postgres naming convention. That's okay. Just have some convention.
22:01
Be consistent. And we're going to just select out of that view. We're only going to select the most recent 24 hours, because in this particular case, we have a reporting app, and we're just going to show the most recent data. And then you can see we have indexes on this materialized view, which is good. Again, you can put this in a migration.
22:21
I would recommend that. The indexes will drop for you automatically. You don't have to specify dropping those. When you drop the materialized view or when you drop a table, it'll drop its indexes for you. Cool. So we have got now some fresh, hot data. But when you materialize that view, that's a one-shot thing.
22:42
You have to go update it. You have to tell the database to update that. So we have this refresh materialized view. And brand new in Postgres 9.4, which dropped in December, we can do that concurrently, which is good, because it would basically table lock. Now, I'm not saying it's going to be fast.
23:01
If you try to select from it while it's updating, but you can't do it. Or it doesn't have to wait for all the selects to clear. So we can update our data now, and everything's great. If you're hosting on Heroku, you can use this, or you can use this Rake task anyways. But if you're hosting on Heroku, use this simple Rake task,
23:21
and you can update it as often as every 10 minutes using the scheduler plug-in. If you need to update it more frequently than that, Heroku's got your back on that. Also, that's a different solution for that. So we're going to just refresh the materialized view, and we've always got up-to-date, reasonably up-to-date data.
23:41
Now, while we're talking about Rake tasks in Heroku, here's a Rake task that I wrote to pull down your production data and load it into your local database. So then I can benchmark my queries against real data. I can run my app against real data.
24:02
This might be a security problem for you in your situation, so maybe you can't do that, and that's fine. But maybe this is helpful for you. There's a gist there, and you're welcome to fork it or clone it or whatever. There's also on postgres.heroku.com, somewhere down kind of in the middle of that page,
24:21
they have a little report where you can see your slow queries, and it, I think, puts it in order and shows you, on average, how bad or good they are. All right, let's keep dancing with data, shall we? We're going to subquery for a field value. This is super cool.
24:41
So this query here is going to do a look ahead. The inner query is going to do a look ahead to see what is the reading ID for, basically, an exit event. When did somebody cross the threshold that I call exit, which is minor zero. So you see, we're selecting from that materialized view twice,
25:05
and we're using that main query to filter the inner query. You can do that. So I just want the next occurrence where the minor is a zero, but is greater than that current record.
25:20
And we're doing that filtering in there, which is why I couldn't do it with a window function. So then we can build up the sunburst chart to see all the places where people went and how often, you know, those paths overlapped. Again, just D3. Cool. Let's talk about some other useful things while
25:41
we're here. If you're the kind of person who likes to read documentation and see what cool things exist, then by all means, check out Ruby's enumerable. There's lots of good stuff in there. Also, look at Postgres data types. There's an array data type where you can take some other data type and put it in an array of things.
26:02
And that's really handy. I'll tell you why here in a second. There's also a date range and a timestamp range data type. So if you're paying close attention, one of those earlier queries did some timeboxing where we wanted to look at something where the start date was whatever and the end date was whatever.
26:21
You can use this date range type where it has a start and end date as a single field in a range, and it has its own operator. So you can ask, give me all the records that exist within this point in time. And those range fields understand that, and they have their own indexes. So that's really handy.
26:41
There's a JSON data type, and then in 9.4, we got a JSON binary just stored in binary instead of JSON. Indexes are a little more efficient with the JSONB also. So if you're familiar with the H store data type, this is like that but actually useful.
27:02
You can do like a nested hierarchy, and you can bury down into it, and with Postgres, you can index anything that you can select from also. So like I said, the JSONB, more efficient index. So you could pull out like all of the values for a field as an array, or you could pull out all the keys as an array, and then Ruby or JavaScript
27:23
or whatever your language is understands how to iterate on collection, you're set like Jell-O. Then we have a UUID data type. So if you're storing UUIDs in a character or a character varying field, maybe you want to look at this. It has its own index type, and you get
27:42
for free validation that it's actually a UUID instead of some goofy thing. So you see there at the bottom, it's kind of old, but that Edge Guides Ruby on Rails site has some good information on how to use, I think, all of those in ActiveRecord and in Rails.
28:02
Future you will thank you. These are good things. So just kind of general how do I Postgres. If you're using OS 10, you have Postgres installed on it, but I couldn't tell you what it is. I haven't ever used it. You can install Postgres and push.js with Homebrew. If you're on Linux, it's super easy with either the YUM
28:23
or the apps repositories, or you could just download Heroku's Postgres.app and you get PostGIS and all these dependencies for free. There are a lot of dependencies for PostGIS. It takes kind of a while to install. The current version of Postgres is 9.4.
28:41
Within, I think it was two weeks of Postgres 9.4 dropping, Heroku was already supporting it, which is pretty amazing. So database tools, like a GUI interface, there's pgadmin3, these are for OS 10. I think pgadmin3 is multi-platform. It's free. The keen bindings, at least on OS 10, are crazy.
29:02
You've also got NaviCat. They've got several different offerings and free trials, and command line PSQL. I use all three of those. And if you're not familiar with the explain analyze command, you need to be familiar with the explain analyze command. So you can run your query against actual data
29:22
and see where are your hotspots, or you can take that and copy it into that website and see your hotspots. That's pretty cool. Okay, so to recap, it's okay to write SQL, really. It's okay.
29:41
We're going to be cool. It's okay. When I refactor, this is kind of the order I go in. Step zero is just get over myself and write something. Get out of my way and just start. And if I see that I've started to kind of mess things and it's getting a little crazy, then maybe I'll reformat that with some common table expression. If I have something that I'm using a lot,
30:02
I'll put that in a view. If I need to squeeze some more performance out, then you've got materialize view. These are just tools that you have in your toolkit that if you didn't know about, now you do. Again, I'm not saying do everything in the database, but it's not a terrible thing when you decide
30:20
that that's what you want to. Just expand your toolbox. You know, insert the more you know. Shooting across the screen here. Postgres is awesome. I'm still finding stuff that is just amazing to me and I can't believe it's in there. So thank you.
Recommendations
Series of 4 media